一、Maven 命令基本结构
Maven 命令通常遵循以下格式:
mvn [options] [<goal(s)>] [<phase(s)>]
[options]
:可选参数,用于修改 Maven 命令的行为,例如-D
用于传递系统属性,-U
用于强制更新依赖等。[<goal(s)>]
:插件目标,指定要执行的具体任务,例如compiler:compile
。[<phase(s)>]
:生命周期阶段,Maven 内置的构建阶段,例如compile
,package
,install
。执行某个阶段会自动执行其之前的所有阶段
🚀 二、核心生命周期命令
Maven 有三个内置的生命周期:clean
, default
(构建), site
(文档)。最常用的是 clean
和 default
。
命令 | 含义 & 作用 |
---|---|
mvn clean |
清理项目,删除 target 目录及其所有构建输出文件(如编译的类文件、打包的 Jar)。 |
mvn compile |
编译项目的主源代码(src/main/java 目录下的 .java 文件),编译后的 .class 文件输出到 target/classes 目录。 |
mvn test |
运行项目的单元测试(src/test/java 目录下的测试类)。它会先自动执行 compile 和 test-compile ,运行测试后通常会生成测试报告。 |
mvn package |
将编译好的代码打包成可分发的格式(例如 JAR、WAR)。它会先执行 compile 和 test 。打包后的文件默认位于 target/ 目录下。 |
mvn install |
将项目打包并安装到本地 Maven 仓库(通常是 ~/.m2/repository/ )。这样,本地其他项目就可以通过 GAV 坐标引用这个 Jar 包了。 |
mvn deploy |
将最终的包部署到远程 Maven 仓库(如公司私服 Nexus 或 Artifactory)。这需要你在 pom.xml 中配置正确的分布式仓库信息。 |
这些命令通常可以组合使用,例如 mvn clean compile
、mvn clean package
,最经典的就是 mvn clean install
,表示先清理再重新编译、测试、打包并安装
📦 三、依赖管理命令
这些命令帮助你查看和管理项目的依赖关系。
命令 | 含义 & 作用 |
---|---|
mvn dependency:tree |
以树形结构显示项目的所有依赖(直接依赖和传递依赖)。这是排查 Jar 包冲突的利器,能清晰看到每个依赖的来源和版本。 |
mvn dependency:analyze |
分析项目依赖,检查是否存在“已声明但未使用”的依赖,或者“未声明但已使用”的依赖(这种情况分析结果会提示,需要注意)。 |
mvn dependency:resolve |
解析并下载项目所需的所有依赖包到本地仓库。 |
mvn dependency:purge-local-repository |
清除本地仓库中未使用的依赖。使用需谨慎,因为它可能会删除其他项目所需的依赖。 |
ℹ️ 四、项目信息与帮助命令
这些命令用于获取项目信息、有效配置或创建新项目。
命令 | 含义 & 作用 |
---|---|
mvn -v 或 mvn --version |
显示 Maven 和 JDK 的版本信息。 |
mvn help:effective-pom |
显示项目合并所有父 POM 和当前配置后的最终有效 POM。当配置层级很多时,这个命令非常有用,可以帮你确认配置是否生效。 |
mvn help:describe |
描述某个插件的信息。例如 mvn help:describe -Dplugin=org.apache.maven.plugins:maven-compiler-plugin 。 |
mvn archetype:generate |
使用 Maven 原型(模板)快速创建一个新项目。它会交互式地引导你输入 GAV 等信息。 |
⚙️ 五、常用参数 (Options)
这些参数可以附加在上述命令之后,以改变其行为。
参数 | 含义 & 作用 | 示例 |
---|---|---|
-Dproperty=value |
传递系统属性或插件属性,用途非常广泛。 | mvn test -Dtest=MyTestClass (只运行MyTestClass 测试类) mvn install -DskipTests (跳过测试) |
-U 或 --update-snapshots |
强制检查并更新远程仓库的快照(SNAPSHOT)依赖。确保你用到的是最新的快照包。 | mvn compile -U |
-X 或 --debug |
开启调试模式,输出非常详细的 Maven 执行信息,用于排查问题。 | mvn install -X |
-q 或 --quiet |
安静模式,只输出错误信息,减少控制台输出。 | mvn compile -q |
-P profile-id |
激活指定的 Maven Profile。Profile 用于定义不同环境(如 dev, prod)的构建配置。 | mvn package -Pprod |
-T threads |
指定构建使用的线程数,进行并行构建以加快速度。 | mvn install -T 4 (用4个线程) |
💡 六、实用技巧与场景示例
跳过测试的几种方式
-DskipTests
: 跳过测试运行,但会编译测试代码。-Dmaven.test.skip=true
: 完全跳过测试,既不编译也不运行测试。
mvn install -DskipTests # 只编译不运行测试 mvn package -Dmaven.test.skip=true # 完全不碰测试
运行单个测试类或方法
mvn test -Dtest=MyTestClass # 运行单个测试类 mvn test -Dtest=MyTestClass#testMethod # 运行单个测试方法 mvn test -Dtest="*MyTest" # 使用通配符
组合命令:清理并安装
mvn clean install # 最常用的组合之一,确保是一次全新的构建安装
查看依赖树并找出冲突
mvn dependency:tree > tree.txt # 将依赖树输出到文件,方便仔细查看 # 在 tree.txt 中搜索冲突的 Jar 包名,查看不同版本是如何被引入的
testng
注解:@Test 标记是用例
属性:
priority=-1 默认测试用例的执行顺序市方法名的ASCII码,值越小优先级越高
enable=false 是否执行用例
description 此方法描述
dataProvider 测试方法的数据提供者名称
alwaysRun=true 如果设置为 true,即使之前调用的一些方法失败或被跳过,此配置方法也将运行
invocationCount=2 测试用例执行次数2
groups={“auth”} 该类/方法所属的组列表
dependsOnGroups={“auth”} 该方法的依赖组列表
dependsOnMethods={“Testlogin”} 测试用例之间的依赖关系
successPercentage 预期此方法的成功率百分比
invocationTimeOut 该测试应花费的最大毫秒数,用于所有调用次数的累积时间
@DataProvider 标记一个方法为提供测试方法数据的方法
- name=“test” 此数据提供者的名称
- parallel=true 测试将在并行中运行
@Factory 标记一个方法作为工厂,该方法返回将被 TestNG 用作测试类的对象。该方法必须返回 Object[]
xml文件中,
tests级别:不同test标签下,用例可以在不同线程执行;相同test标签下,用例在用一个线程执行
method级别:所有用例在不同线程下执行
class级别:不同class标签下,用例可以在不同线程执行;相同clas标签下,用例在用一个线程执行
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<!--<suite name="TestSuite" parallel="tests" thread-count="3">-->
<suite name="TestSuite" preserve-order="true">
<parameter name="first-name" value="Cedric"/>
<!-- 定义可在测试中使用的参数 -->
<parameter name="browser" value="chrome"/>
<parameter name="environment" value="staging"/>
<test name="注册模块">
<classes>
<class name="com.register.TestRegister"/>
</classes>
</test>
<test name="登录模块1">
<classes>
<class name="com.login.TestLogin1"/>
<parameter name="username" value="admin"></parameter>
<parameter name="password" value="123"></parameter>
</classes>
</test>
<test name="登录模块2">
<classes>
<class name="com.login.TestLogin2"/>
</classes>
</test>
<test name="计算模块">
<classes>
<class name="com.example.CalculatorTest"/>
</classes>
</test>
<!-- 运行 Smoke 和 Regression 测试 ,排除除integration外-->
<test name="Run Smoke and Regression Tests">
<groups>
<run>
<include name="smoke"/>
<include name="regression"/>
<exclude name="integration"/>
</run>
</groups>
<classes>
<class name="com.example.CalculatorTest"/>
</classes>
</test>
<!-- 使用元组(MetaGroups)定义复杂的分组逻辑 -->
<test name="Run Custom Group Combinations">
<groups>
<define name="all-core-tests">
<include name="smoke"/>
<include name="regression"/>
</define>
<run>
<include name="all-core-tests"/>
</run>
</groups>
<classes>
<class name="com.example.CalculatorTest"/>
</classes>
</test>
</suite>
生成测试报告
1:引入依赖库,见下方pom.xml
testng.xml中编写不同路径下的套件执行内容
2:运行测试,生成测试结果的json文件
mvn test clean test
3:生成HTML报告
mvn io.qameta.allure:allure-maven:serve
allure
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.yourcompany</groupId>
<artifactId>your-test-project</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<!-- 统一编码防止乱码 -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<aspectj.version>1.9.7</aspectj.version>
<allure.version>2.17.3</allure.version> <!-- 或 2.13.8 -->
</properties>
<dependencies>
<!-- TestNG -->
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>7.4.0</version>
<scope>test</scope>
</dependency>
<!-- Allure TestNG 适配器 -->
<dependency>
<groupId>io.qameta.allure</groupId>
<artifactId>allure-testng</artifactId>
<version>${allure.version}</version> <!-- 版本由上面的property控制 -->
<scope>test</scope>
</dependency>
<!-- AspectJ 用于支持 Allure 的步骤记录 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>${aspectj.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Maven Surefire 插件 (用于执行 TestNG 测试) -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M7</version>
<configuration>
<!-- 配置argLine以支持Allure监听器和AspectJ编织器 -->
<argLine>
-javaagent:"${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar"
</argLine>
<systemProperties>
<property>
<!-- 指定Allure结果目录 -->
<name>allure.results.directory</name>
<value>${project.build.directory}/allure-results</value>
</property>
</systemProperties>
</configuration>
</plugin>
<!-- Allure Maven 插件 (用于生成和展示报告) -->
<plugin>
<groupId>io.qameta.allure</groupId>
<artifactId>allure-maven</artifactId>
<version>2.10.0</version>
<configuration>
<reportVersion>${allure.version}</reportVersion>
</configuration>
</plugin>
</plugins>
</build>
</project>
extentreports
1:导入依赖extentreports
<dependency>
<groupId>com.vimalselvam</groupId>
<artifactId>testng-extentsreport</artifactId>
<version>1.3.1</version>
</dependency>
<dependency>
<groupId>com.aventstack</groupId>
<artifactId>extentreports</artifactId>
<version>3.0.6</version>
</dependency>
2:重写IReporter 的generateReport方法
3:xml文件引入监听器
<listeners>
<listener class-name="com.example.ExtentTestNGIReporterListener"></listener>
</listeners>
//package com.welab.automation.framework.listener;
import com.aventstack.extentreports.ExtentReports;
import com.aventstack.extentreports.ExtentTest;
import com.aventstack.extentreports.ResourceCDN;
import com.aventstack.extentreports.Status;
import com.aventstack.extentreports.model.TestAttribute;
import com.aventstack.extentreports.reporter.ExtentHtmlReporter;
import com.aventstack.extentreports.reporter.configuration.ChartLocation;
import com.aventstack.extentreports.reporter.configuration.Theme;
import org.testng.*;
import org.testng.xml.XmlSuite;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.*;
public class ExtentTestNGIReporterListener implements IReporter {
static SimpleDateFormat format=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
static String dataNow=format.format(new Date());
//生成的路径以及文件名
private static final String OUTPUT_FOLDER = "test-output/";
private static final String FILE_NAME = "测试报告.html";
private static final String DocumentTitle = "api自动化测试报告";
private static final String ReportName = "api自动化测试报告";
private ExtentReports extent;
@Override
public void generateReport(List<XmlSuite> xmlSuites, List<ISuite> suites, String outputDirectory) {
init();
boolean createSuiteNode = false;
if(suites.size()>1){
createSuiteNode=true;
}
for (ISuite suite : suites) {
Map<String, ISuiteResult> result = suite.getResults();
//如果suite里面没有任何用例,直接跳过,不在报告里生成
if(result.size()==0){
continue;
}
//统计suite下的成功、失败、跳过的总用例数
int suiteFailSize=0;
int suitePassSize=0;
int suiteSkipSize=0;
ExtentTest suiteTest=null;
//存在多个suite的情况下,在报告中将同一个一个suite的测试结果归为一类,创建一级节点。
if(createSuiteNode){
suiteTest = extent.createTest(suite.getName()).assignCategory(suite.getName());
}
boolean createSuiteResultNode = false;
if(result.size()>1){
createSuiteResultNode=true;
}
for (ISuiteResult r : result.values()) {
ExtentTest resultNode;
ITestContext context = r.getTestContext();
if(createSuiteResultNode){
//没有创建suite的情况下,将在SuiteResult的创建为一级节点,否则创建为suite的一个子节点。
if( null == suiteTest){
resultNode = extent.createTest(r.getTestContext().getName());
}else{
resultNode = suiteTest.createNode(r.getTestContext().getName());
}
}else{
resultNode = suiteTest;
}
if(resultNode != null){
resultNode.getModel().setName(suite.getName()+" : "+r.getTestContext().getName());
System.out.println("suite.getName()-->"+suite.getName()+"\tr.getTestContext().getName()-->"+r.getTestContext().getName());
if(resultNode.getModel().hasCategory()){
resultNode.assignCategory(r.getTestContext().getName());
}else{
resultNode.assignCategory(suite.getName(),r.getTestContext().getName());
}
resultNode.getModel().setStartTime(r.getTestContext().getStartDate());
resultNode.getModel().setEndTime(r.getTestContext().getEndDate());
//统计SuiteResult下的数据
int passSize = r.getTestContext().getPassedTests().size();
int failSize = r.getTestContext().getFailedTests().size();
int skipSize = r.getTestContext().getSkippedTests().size();
suitePassSize += passSize;
suiteFailSize += failSize;
suiteSkipSize += skipSize;
if(failSize>0){
resultNode.getModel().setStatus(Status.FAIL);
}
resultNode.getModel().setDescription(String.format("Pass: %s ; Fail: %s ; Skip: %s ;",passSize,failSize,skipSize));
}
buildTestNodes(resultNode,context.getFailedTests(), Status.FAIL);
buildTestNodes(resultNode,context.getSkippedTests(), Status.SKIP);
buildTestNodes(resultNode,context.getPassedTests(), Status.PASS);
}
if(suiteTest!= null){
suiteTest.getModel().setDescription(String.format("Pass: %s ; Fail: %s ; Skip: %s ;",suitePassSize,suiteFailSize,suiteSkipSize));
if(suiteFailSize>0){
suiteTest.getModel().setStatus(Status.FAIL);
}
}
}
// for (String s : Reporter.getOutput()) {
// extent.setTestRunnerOutput(s);
// }
extent.flush();
}
private void init() {
//文件夹不存在的话进行创建
File reportDir= new File(OUTPUT_FOLDER);
if(!reportDir.exists()&& !reportDir .isDirectory()){
reportDir.mkdir();
}
ExtentHtmlReporter htmlReporter = new ExtentHtmlReporter(OUTPUT_FOLDER + FILE_NAME);
// 设置静态文件的DNS
//怎么样解决cdn.rawgit.com访问不了的情况
htmlReporter.config().setResourceCDN(ResourceCDN.EXTENTREPORTS);
htmlReporter.config().setDocumentTitle(DocumentTitle);
htmlReporter.config().setReportName(ReportName);
htmlReporter.config().setChartVisibilityOnOpen(true);
htmlReporter.config().setTestViewChartLocation(ChartLocation.TOP);
htmlReporter.config().setTheme(Theme.STANDARD);
htmlReporter.config().setCSS(".node.level-1 ul{ display:none;} .node.level-1.active ul{display:block;}");
extent = new ExtentReports();
extent.attachReporter(htmlReporter);
extent.setReportUsesManualConfiguration(true);
}
private void buildTestNodes(ExtentTest extenttest, IResultMap tests, Status status) {
//存在父节点时,获取父节点的标签
String[] categories=new String[0];
if(extenttest != null ){
List<TestAttribute> categoryList = extenttest.getModel().getCategoryContext().getAll();
categories = new String[categoryList.size()];
for(int index=0;index<categoryList.size();index++){
categories[index] = categoryList.get(index).getName();
}
}
ExtentTest test;
if (tests.size() > 0) {
//调整用例排序,按时间排序
Set<ITestResult> treeSet = new TreeSet<ITestResult>(new Comparator<ITestResult>() {
@Override
public int compare(ITestResult o1, ITestResult o2) {
return o1.getStartMillis()<o2.getStartMillis()?-1:1;
}
});
treeSet.addAll(tests.getAllResults());
for (ITestResult result : treeSet) {
Object[] parameters = result.getParameters();
String name="";
//如果有参数,则使用参数的toString组合代替报告中的name
for(Object param:parameters){
name+=param.toString();
}
if(name.length()>0){
if(name.length()>50){
name= name.substring(0,49)+"...";
}
}else{
name = result.getMethod().getMethodName();
}
if(extenttest==null){
test = extent.createTest(name);
}else{
//作为子节点进行创建时,设置同父节点的标签一致,便于报告检索。
test = extenttest.createNode(name).assignCategory(categories);
}
//test.getModel().setDescription(description.toString());
//test = extent.createTest(result.getMethod().getMethodName());
for (String group : result.getMethod().getGroups()) {
test.assignCategory(group);
test.assignCategory(result.getMethod().getDescription());//描述
}
List<String> outputList = Reporter.getOutput(result);
for(String output:outputList){
//将用例的log输出报告中
test.debug(output);
}
if (result.getThrowable() != null) {
test.log(status, result.getThrowable());
}
else {
test.log(status, "Test " + status.toString().toLowerCase() + "ed");
}
test.getModel().setStartTime(getTime(result.getStartMillis()));
test.getModel().setEndTime(getTime(result.getEndMillis()));
}
}
}
private Date getTime(long millis) {
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(millis);
return calendar.getTime();
}
}