3 分鐘生成一個單元測試報告,這個樣式愛了
昨天有個小夥伴問我,有沒有什麼現成的測試報告模板,由於昨天實在比較忙就沒顧上,所以今個有時間趕緊補上。一般力所能及的事,只要我有時間都會為大家解決,但畢竟能力有限做不到的地方小夥伴們也多理解。
平時我們開發介面時,Junit
單元測試是最為常用的一種開發測試手段,很多時候測試其實只看介面是否正常返回結果就 ok 了。但有時間我們要測試一些特殊場景,如:介面超時測試等,就沒什麼太好的辦法了,而 TestNG
實現容易的多。它與 JUnit
用法十分相似,只要你用過 JUnit
分分鐘上手。
大致講一下 TestNG
的幾個重要概念,@Test
註解標註的方法是最小的執行單元,我們可以將這些單個的測試用例劃分成 group
group
可以用在測試類或者方法上,suite
套件可以理解成測試類的容器。下邊我們搭建一個TestNG
測試框架,結合具體案例介紹一下它的功能。
核心依賴
引入 extentreports 和 testng
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.testng</groupId> <artifactId>testng</artifactId> <version>7.1.0</version> <scope>test</scope> </dependency> <dependency> <groupId>com.aventstack</groupId> <artifactId>extentreports</artifactId> <version>3.0.6</version> </dependency> </dependencies>
TestNG 配置
TestNG
支援兩種執行方式,第一種是用註解像 Junit
直接點方法名 run
執行。第二種配置 xml
檔案的方式。
@Slf4j @Listeners({ExtentTestNGIReporterListener.class}) @SpringBootTest(classes = SpringbootTestngReportApplication.class) public class UserTest extends AbstractTestNGSpringContextTests { @Data class User { private Integer userId; private String userName; } /** * 引數提供 */ @DataProvider(name = "paramDataProvider") public Object[][] paramDataProvider() { User user1 = new User(); user1.setUserId(1); user1.setUserName("程式設計師內點事1"); User user2 = new User(); user2.setUserId(2); user2.setUserName("程式設計師內點事2"); return new Object[][]{{1, user1}, {2, user2}}; } @Test(dataProvider = "paramDataProvider") public void queryUser(Integer index, User user) { if (index == 2) { int a = 1 / 0; } log.info("index:{},user: {}", index, JSON.toJSONString(user)); Assert.assertTrue(Objects.nonNull(user)); } }
xml
方式直接右鍵 .xml
檔案 run
就運行了。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite name="使用者單元測試" parallel="classes" thread-count="5">
<listeners>
<listener class-name="com.xiaofu.report.config.ExtentTestNGIReporterListener"/>
</listeners>
<test verbose="1" name="使用者測試">
<parameter name="userId" value="1"/>
<parameter name="userName" value="程式設計師內點事"/>
<groups>
<define name="queryUser"/>
<define name="queryUser1"/>
</groups>
<classes>
<class name="com.xiaofu.report.UserTest"/>
</classes>
</test>
</suite>
測試報告配置
手動配置一個測試報告偵聽器類 ExtentTestNGIReporterListener
,可以自行定義在測試報告上顯示的資料,最後執行測試方法同時會生成測試報告。
/**
* @author xiaofu
* @description TestNg 視覺化配置
* @date 2020/3/19 16:44
*/
public class ExtentTestNGIReporterListener implements IReporter {
//生成的路徑以及檔名
private static final String OUTPUT_FOLDER = "target/test-report/";
private static final String FILE_NAME = "index.html";
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());
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.mkdirs();
}
ExtentHtmlReporter htmlReporter = new ExtentHtmlReporter(OUTPUT_FOLDER + FILE_NAME);
// 設定靜態檔案的DNS
//怎麼樣解決cdn.rawgit.com訪問不了的情況
htmlReporter.config().setResourceCDN(ResourceCDN.EXTENTREPORTS);
htmlReporter.config().setDocumentTitle("使用者服務自動化測試報告");
htmlReporter.config().setReportName("使用者服務自動化測試報告");
htmlReporter.config().setChartVisibilityOnOpen(true);
htmlReporter.config().setTestViewChartLocation(ChartLocation.TOP);
htmlReporter.config().setTheme(Theme.STANDARD);
htmlReporter.config().setEncoding("utf-8");
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) {
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);
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();
}
}
會在指定的目錄 target/test-report/
下生成 index.html
測試報告檔案,測試的成功率等資訊顯示的都比較直觀,樣式也還是蠻好看。
測試場景
下邊就簡單介紹幾個我常用的 testNG 測試場景
1、引數化測試
使用 @DataProvider
註解為其他測試方法提供引數,queryUser
方法會執行 Object[][]
陣列中所有引數user1 、user2,相當於迴圈執行測試方法。
@DataProvider(name = "paramDataProvider")
public Object[][] paramDataProvider() {
User user1 = new User();
user1.setUserId(1);
user1.setUserName("程式設計師內點事1");
User user2 = new User();
user2.setUserId(2);
user2.setUserName("程式設計師內點事2");
return new Object[][]{{1, user1}, {2, user2}};
}
@Test(dataProvider = "paramDataProvider",groups = "user")
public void queryUser(Integer index, User user) {
log.info("index:{},user: {}", index, JSON.toJSONString(user));
}
xml 方式下還可以在配置檔案設定引數
<parameter name="name" value="程式設計師內點事"/>
@Test(groups = "user")
public void queryUser(String name) {
log.info("我是測試方法~");
}
2、超時測試
可以給測試方法一個超時時間,如果實際執行時間超過設定的超時時間,用例將不通過。
@Test(timeOut = 5000)
public void timeOutTest() throws InterruptedException {
Thread.sleep(6000);
}
3、依賴測試
有時我們可能需要以特定順序呼叫測試用例中的方法,或者希望在方法之間共享一些資料,TestNG
支援在測試方法之間顯式依賴的宣告。
@Test
public void token() {
System.out.println("get token");
}
@Test(dependsOnMethods= {"token"})
public void getUser() {
System.out.println("this is test getUser");
}
總結
簡單提了一下 TestNG
框架相關的知識,說實話本來就為給老鐵弄個測試報告模板,一不留神說這麼多。如果小夥伴們對這個測試框架感興趣,下次我會出一份詳細的 TestNG
文章。
原創不易,燃燒秀髮輸出內容,如果有一丟丟收穫,點個贊鼓勵一下吧!
整理了幾百本各類技術電子書,送給小夥伴們。關注公號回覆【666】自行領取。和一些小夥伴們建了一個技術交流群,一起探討技術、分享技術資料,旨在共同學習進步,如果感興趣就加入我們吧!