NỘI DUNG BÀI HỌC
✅ Các loại Listeners trong TestNG
✅ ITestListener trong TestNG
✅ Cách triển khai ITestListener trong TestNG
✅ TestNG Listeners là gì?
Điều đầu tiên xuất hiện trong đầu khi đọc thuật ngữ "Listeners" là hiểu như nó đang lắng nghe một thứ gì đó 😁
TestNG Listeners là đoạn mã được thiết kế sẵn để đọc hiểu các sự kiện xảy ra trong TestNG. Nếu sự kiện khớp với sự kiện mà chúng ta muốn Listeners hiểu thì nó sẽ thực thi mã code đó cho chúng ta.
Điều này sẽ dẫn đến việc sửa đổi hành vi mặc định của TestNG.
Ví dụ, chúng ta chỉ muốn in lỗi ngoại lệ lên các báo cáo nếu thử nghiệm không thành công. Ở đây, chúng ta có thể áp dụng một TestNG Listeners sẽ theo dõi và hiểu sự kiện "fail of test case" và khi nó xảy ra, nó sẽ ghi lại lỗi đó. Mình sẽ custom tùy ý với các loại trong Listeners của nó hỗ trợ.
Listeners TestNG được áp dụng làm giao diện trong code vì Listeners cũng chỉ là một interface trong TestNG. Nó cung cấp cho chúng ta các hàm xử lý trong giao diện class Listener đó để lắng nghe được nhịp điệu trạng thái khi mà mình bắt đầu chạy test.
Nó được sử dụng trong Selenium bằng cách triển khai giao diện Listeners (Interface). Nó cho phép tùy chỉnh các báo cáo hoặc nhật ký trong TestNG. Có nhiều loại trình nghe TestNG có sẵn chúng ta sẽ thảo luận về những điều này trong phần tiếp theo.
✅ Các loại Listeners trong TestNG
- ITestListener
- IReporter
- ISuiteListener
- IInvokedMethod
- IHookable
- IConfigurationListener
- IConfigurableListener
- IAnnotationTransformer
- IExecution
- IMethodInterceptor
Chúng ta sẽ học:
- ITestListener
Trình lắng nghe cho các test cases.
✅ ITestListener trong TestNG
ITestListener có các phương pháp sau:
onStart: Phương thức này được gọi khi lớp thử nghiệm được khởi tạo và trước khi thực thi bất kỳ phương thức thử nghiệm nào (@Test).
Cú pháp: void onStart (ITestContext context){...}
onFinish: Phương thức này được gọi sau khi tất cả các phương thức thử nghiệm đã chạy và việc gọi tất cả các phương thức cấu hình của chúng sẽ xảy ra.
Cú pháp: void onFinish (ITestContext context){...}
onTestStart: Phương thức này được gọi mỗi khi Test Method (@Test) được gọi và thực thi.
Cú pháp: void onTestStart (ITestResult result){...}
onTestSuccess: Phương thức này được gọi mỗi khi Test Method chạy passed (thành công).
Cú pháp: void onTestSuccess (ITestResult result){...}
onTestFailure: Phương thức này gọi mỗi khi Test Method chạy bị Fail (không thành công).
Cú pháp: void onTestFailure (ITestResult result){...}
onTestSkipped: Phương thức này được gọi mỗi khi Test Method bị bỏ qua (Skipped/ Ignored).
Cú pháp: void onTestSkipped (ITestResult result){...}
onTestFailedButWithinSuccessPercentage: Phương thức này gọi khi phương pháp thử nghiệm không thành công toàn bộ nhưng đã vượt qua một phần trăm thành công nhất định, được xác định bởi người dùng.
Cú pháp: void onTestFailedButSuccessPercentage (ITestResult result){...}
✅ Triển khai ITestListener trong TestNG
Phần này sẽ giải thích từng bước cách sử dụng trình lắng nghe trong TestNG để gọi các chức năng khác nhau. Điều quan trọng cần lưu ý ở đây là Người nghe có thể triển khai theo hai cách trong TestNG:
- Ở cấp độ Class: Sử dụng annotation @Listeners trên mỗi class testcase.
- Ở cấp độ Suite: Sử dụng tag <Listeners> trong tệp XML TestNG.
🔆 Triển khai ITestListener ở cấp độ Class
Để triển khai ITestListener, chúng ta cần tạo một lớp với tất cả các phương thức mà chúng ta muốn ghi đè. Ví dụ đặt nó là TestListener.java
Nhớ là ghi đè thì phải có @Override vì mình đang gọi lại từ interface ITestListener.
import org.testng.ITestContext;
import org.testng.ITestListener;
import org.testng.ITestResult;
public class TestListener implements ITestListener {
@Override
public void onStart(ITestContext arg0) {
// TODO Auto-generated method stub
}
@Override
public void onFinish(ITestContext arg0) {
// TODO Auto-generated method stub
}
@Override
public void onTestStart(ITestResult arg0) {
// TODO Auto-generated method stub
}
@Override
public void onTestSuccess(ITestResult arg0) {
// TODO Auto-generated method stub
}
@Override
public void onTestFailure(ITestResult arg0) {
// TODO Auto-generated method stub
}
@Override
public void onTestSkipped(ITestResult arg0) {
// TODO Auto-generated method stub
}
}
Và bây giờ chúng ta muốn custom thằng nào thì sửa trong nội dung hàm của thằng đó thôi.
Ví dụ sửa các hàm bên dưới với dòng lệnh in ra tên test cases chẳng hạn:
package com.anhtester.listeners;
import org.testng.ITestContext;
import org.testng.ITestListener;
import org.testng.ITestResult;
public class TestListener implements ITestListener {
@Override
public void onStart(ITestContext result) {
System.out.println("♻\uFE0F Setup môi trường: " + result.getStartDate());
}
@Override
public void onFinish(ITestContext result) {
System.out.println("\uD83D\uDD06 Kết thúc chạy test: " + result.getEndDate());
}
@Override
public void onTestStart(ITestResult result) {
System.out.println("➡\uFE0F Bắt đầu chạy test case: " + result.getName());
}
@Override
public void onTestSuccess(ITestResult result) {
System.out.println("✅ Test case " + result.getName() + " is passed.");
System.out.println("==> Status: " + result.getStatus());
}
@Override
public void onTestFailure(ITestResult result) {
System.out.println("❌ Test case " + result.getName() + " is failed.");
System.out.println("==> Status: " + result.getStatus());
}
@Override
public void onTestSkipped(ITestResult result) {
System.out.println("⛔\uFE0F Test case " + result.getName() + " is skipped.");
System.out.println("==> Status: " + result.getStatus());
}
}
🔆 Gọi lại TestListener sử dụng trong class test cases
Tiếp theo tạo class test case để gọi lại TestListener. Tạo class tên DemoListener chẳng hạn.
import com.anhtester.listeners.TestListener;
import org.testng.SkipException;
import org.testng.annotations.Listeners;
import org.testng.annotations.Test;
@Listeners(TestListener.class)
public class DemoListener {
@Test
public void test1() {
System.out.println("Test 1 running...");
}
@Test
public void test2() {
System.out.println("Test 2 running...");
throw new RuntimeException("Test 2 failed");
}
@Test
public void test3() {
System.out.println("Test 3 running...");
throw new SkipException("Test 3 skipped");
}
}
Các bạn chú ý chỗ @Listeners(TestListener.class)
@Listeners(tên class Listener mà mình đã setup ở trên hoặc class Listener nào đó khác)
Nhớ chấm class ở đuôi. Syntax:
@Listeners(PackageName.ClassName.class)
nếu trỏ đến package rồi thì không cần để package vào cũng được.
Mình để nó ở trên đầu của class Test cases.
Và bây giờ chạy thử class test case xem kết quả.
KẾT QUẢ:
♻️ Setup môi trường: Tue Apr 22 14:30:20 ICT 2025
➡️ Bắt đầu chạy test case: test1
Test 1 running...
✅ Test case test1 is passed.
==> Status: 1
➡️ Bắt đầu chạy test case: test2
Test 2 running...
❌ Test case test2 is failed.
==> Status: 2
java.lang.RuntimeException: Test 2 failed
at com.anhtester.Bai24_Listeners.DemoListener.test2(DemoListener.java:19)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:569)
at org.testng.internal.invokers.MethodInvocationHelper.invokeMethod(MethodInvocationHelper.java:141)
at org.testng.internal.invokers.TestInvoker.invokeMethod(TestInvoker.java:687)
at org.testng.internal.invokers.TestInvoker.invokeTestMethod(TestInvoker.java:230)
at org.testng.internal.invokers.MethodRunner.runInSequence(MethodRunner.java:63)
at org.testng.internal.invokers.TestInvoker$MethodInvocationAgent.invoke(TestInvoker.java:995)
at org.testng.internal.invokers.TestInvoker.invokeTestMethods(TestInvoker.java:203)
at org.testng.internal.invokers.TestMethodWorker.invokeTestMethods(TestMethodWorker.java:154)
at org.testng.internal.invokers.TestMethodWorker.run(TestMethodWorker.java:134)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
at org.testng.TestRunner.privateRun(TestRunner.java:741)
at org.testng.TestRunner.run(TestRunner.java:616)
at org.testng.SuiteRunner.runTest(SuiteRunner.java:421)
at org.testng.SuiteRunner.runSequentially(SuiteRunner.java:413)
at org.testng.SuiteRunner.privateRun(SuiteRunner.java:373)
at org.testng.SuiteRunner.run(SuiteRunner.java:312)
at org.testng.SuiteRunnerWorker.runSuite(SuiteRunnerWorker.java:52)
at org.testng.SuiteRunnerWorker.run(SuiteRunnerWorker.java:95)
at org.testng.TestNG.runSuitesSequentially(TestNG.java:1274)
at org.testng.TestNG.runSuitesLocally(TestNG.java:1208)
at org.testng.TestNG.runSuites(TestNG.java:1112)
at org.testng.TestNG.run(TestNG.java:1079)
at com.intellij.rt.testng.IDEARemoteTestNG.run(IDEARemoteTestNG.java:65)
at com.intellij.rt.testng.RemoteTestNGStarter.main(RemoteTestNGStarter.java:105)
➡️ Bắt đầu chạy test case: test3
Test 3 running...
⛔️ Test case test3 is skipped.
==> Status: 3
Test ignored.
🔆 Kết thúc chạy test: Tue Apr 22 14:30:20 ICT 2025
===============================================
Default Suite
Total tests run: 3, Passes: 1, Failures: 1, Skips: 1
===============================================
Nó đã thực thi các câu thông báo như mình đã thiết lập sẵn. Và mình thấy nó mapping với nhau hết các sự kiện đã thiết lập.
🔆 Gọi lại TestListener tại class BaseTest
Chúng ta có thể gọi TestListener tại class BaseTest để tiện khỏi phải khai báo trong từng class test cases, vì class BaseTest sẽ được các class test cases khác kế thừa lại, như vậy chỉ cần khai báo một lần tại class BaseTest thì nó sẽ có hiệu lực cho tất cả.
Chú ý: Có thể BaseTest dùng tên khác trong quá trình học không phải là BaseTest, chẳng hạn BaseTest_Json_Device, tuỳ nội dung đến giai đoạn nào.
Xoá bỏ TestListener trong từng class test cases.
Thực tế hơn chút thì mình có thể gọi các hàm screenshot hay record screen video khi test case fail chẳng hạn hoặc tất cả các test cases. Hoặc là gửi Mail, xuất Report sau khi chạy test.
Thường thì Screenshots để nó ở chỗ Fail hợp lý hơn, vì lỗi mới chụp màn hình lại làm bằng chứng chứ nó passed rồi thì thôi chụp chi tùm lum cũng không cần thiết.
Ví dụ:
@Override
public void onTestFailure(ITestResult result) {
System.out.println("Đây là test case bị fail: " + result.getName());
CaptureHelpers.captureScreenshot(result.getName());
}
Ví dụ khác về phương thức onTestFailedButWithinSuccessPercentage trong ITestListener:
Như đã nói bên trên thì thằng này nó sẽ cho biết được thông tin @Test những lần failed nhưng trong đó có phần trăm đã passed thông qua số lần chạy của @Test đó.
Hiểu nôm na chạy test case Login 5 lần mà có 2 lần fail và 3 lần pass thì nó sẽ lấy thông tin của những lần fail đó.
Sử dụng thuộc tính "successPercentage" trong @Test, bạn có thể chỉ định phần trăm thành công được mong đợi từ phương pháp này. Mặc định là (100) tương ứng 100%. Và thuộc tính "invocationCount" để thiết lập số lần chạy của test đó.
@Listeners(TestListener.class)
public class SuccessPercentageDemo {
int count = 0;
@Test(invocationCount = 5, successPercentage = 50)
public void kiemTraChanLe() {
count++;
System.out.println("Số lần chạy: " + count);
if (count % 2 == 0) {
Assert.assertTrue(false);
} else {
Assert.assertTrue(true);
}
}
}
Nhưng ví dụ trên thì An sẽ tạo biến đếm toàn cục để biết @Test nó chạy bao nhiêu lần.
Truyền vào hàm có hai thuộc tính thiết lặp như ý nghĩa trên. Nó sẽ chạy 5 lần và trong tỷ lệ phần trăm thành công lớn hơn hoặc bằng 50%.
À còn cái phép tính thì đại khái là nó lấy số lần chạy count đó đem chia cho 2 lấy phần dư để so sánh bằng với 0. (giống kiểm tra số chẵn lẻ ý mà). Trong đó tính mắt thì thấy là có 2 lần Fail (count = 2 và count = 4).
Ừ mà thôi kệ bà nó đừng quan tâm nó làm cái gì. Quan tâm cái kết quả nó chạy có lần nào fail và pass thôi là được rồi vì sau này chạy test case đâu có thường tính ba thứ này đâu chứ hả.
Và cái tiếp theo quan trọng là nhớ ghi cái gì đó vào chổ method onTestFailedButWithinSuccessPercentage trong ITestListener để dễ nhận biết dấu hiệu là nó có xác nhận được trạng thái xảy ra bên trên nhé.
public class TestListener implements ITestListener {
public void onTestFailedButWithinSuccessPercentage(ITestResult result) {
System.out.println("Tên của testcase failed nhưng có phần trăm passed là:" + result.getName());
}
}
Kết quả:
ChromeDriver was started successfully.
thg 12 17, 2021 3:16:05 SA org.openqa.selenium.remote.ProtocolHandshake createSession
INFO: Detected dialect: W3C
Số lần chạy: 1
Số lần chạy: 2
Tên của testcase failed nhưng có phần trăm passed là:kiemTraChanLe
java.lang.AssertionError: expected [true] but found [false]
at org.testng.Assert.fail(Assert.java:99)
at org.testng.Assert.failNotEquals(Assert.java:1037)
at org.testng.Assert.assertTrue(Assert.java:45)
at org.testng.Assert.assertTrue(Assert.java:55)
at anhtester.com.testcases.ListenerTC.kiemTraChanLe(ListenerTC.java:50)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:64)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:564)
at org.testng.internal.MethodInvocationHelper.invokeMethod(MethodInvocationHelper.java:133)
at org.testng.internal.TestInvoker.invokeMethod(TestInvoker.java:598)
at org.testng.internal.TestInvoker.invokeTestMethod(TestInvoker.java:173)
at org.testng.internal.MethodRunner.runInSequence(MethodRunner.java:46)
at org.testng.internal.TestInvoker$MethodInvocationAgent.invoke(TestInvoker.java:824)
at org.testng.internal.TestInvoker.invokeTestMethods(TestInvoker.java:146)
at org.testng.internal.TestMethodWorker.invokeTestMethods(TestMethodWorker.java:146)
at org.testng.internal.TestMethodWorker.run(TestMethodWorker.java:128)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
at org.testng.TestRunner.privateRun(TestRunner.java:794)
at org.testng.TestRunner.run(TestRunner.java:596)
at org.testng.SuiteRunner.runTest(SuiteRunner.java:377)
at org.testng.SuiteRunner.runSequentially(SuiteRunner.java:371)
at org.testng.SuiteRunner.privateRun(SuiteRunner.java:332)
at org.testng.SuiteRunner.run(SuiteRunner.java:276)
at org.testng.SuiteRunnerWorker.runSuite(SuiteRunnerWorker.java:53)
at org.testng.SuiteRunnerWorker.run(SuiteRunnerWorker.java:96)
at org.testng.TestNG.runSuitesSequentially(TestNG.java:1212)
at org.testng.TestNG.runSuitesLocally(TestNG.java:1134)
at org.testng.TestNG.runSuites(TestNG.java:1063)
at org.testng.TestNG.run(TestNG.java:1031)
at com.intellij.rt.testng.IDEARemoteTestNG.run(IDEARemoteTestNG.java:66)
at com.intellij.rt.testng.RemoteTestNGStarter.main(RemoteTestNGStarter.java:109)
Số lần chạy: 3
Số lần chạy: 4
java.lang.AssertionError: expected [true] but found [false]
Tên của testcase failed nhưng có phần trăm passed là:kiemTraChanLe
at org.testng.Assert.fail(Assert.java:99)
at org.testng.Assert.failNotEquals(Assert.java:1037)
at org.testng.Assert.assertTrue(Assert.java:45)
at org.testng.Assert.assertTrue(Assert.java:55)
at anhtester.com.testcases.ListenerTC.kiemTraChanLe(ListenerTC.java:50)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:64)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:564)
at org.testng.internal.MethodInvocationHelper.invokeMethod(MethodInvocationHelper.java:133)
at org.testng.internal.TestInvoker.invokeMethod(TestInvoker.java:598)
at org.testng.internal.TestInvoker.invokeTestMethod(TestInvoker.java:173)
at org.testng.internal.MethodRunner.runInSequence(MethodRunner.java:46)
at org.testng.internal.TestInvoker$MethodInvocationAgent.invoke(TestInvoker.java:824)
at org.testng.internal.TestInvoker.invokeTestMethods(TestInvoker.java:146)
at org.testng.internal.TestMethodWorker.invokeTestMethods(TestMethodWorker.java:146)
at org.testng.internal.TestMethodWorker.run(TestMethodWorker.java:128)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
at org.testng.TestRunner.privateRun(TestRunner.java:794)
at org.testng.TestRunner.run(TestRunner.java:596)
at org.testng.SuiteRunner.runTest(SuiteRunner.java:377)
at org.testng.SuiteRunner.runSequentially(SuiteRunner.java:371)
at org.testng.SuiteRunner.privateRun(SuiteRunner.java:332)
at org.testng.SuiteRunner.run(SuiteRunner.java:276)
at org.testng.SuiteRunnerWorker.runSuite(SuiteRunnerWorker.java:53)
at org.testng.SuiteRunnerWorker.run(SuiteRunnerWorker.java:96)
at org.testng.TestNG.runSuitesSequentially(TestNG.java:1212)
at org.testng.TestNG.runSuitesLocally(TestNG.java:1134)
at org.testng.TestNG.runSuites(TestNG.java:1063)
at org.testng.TestNG.run(TestNG.java:1031)
at com.intellij.rt.testng.IDEARemoteTestNG.run(IDEARemoteTestNG.java:66)
at com.intellij.rt.testng.RemoteTestNGStarter.main(RemoteTestNGStarter.java:109)
Số lần chạy: 5
===============================================
Default Suite
Total tests run: 5, Passes: 3, Failures: 2, Skips: 0
===============================================
Nó báo sai ở lần chạy 2 và 4 như mình cố tình thiết lập trước đó.
🔆 Triển khai ITestListener ở cấp độ Suite
Để triển khai ITestListener ở cấp Suite, thì chúng ta xóa chú thích @Listeners khỏi class Test case đi và thiết lập nó trong tệp XML.
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >
<suite name="Suite Regression Test" verbose="1">
<listeners>
<listener class-name="com.anhtester.listeners.TestListener"/>
</listeners>
<test name="Test Listener TestNG">
<parameter name="platformName" value="Android"/>
<parameter name="deviceName" value="pixel9"/>
<parameter name="udid" value="emulator-5554"/>
<parameter name="host" value="127.0.0.1"/>
<parameter name="port" value="9000"/>
<parameter name="systemPort" value="9201"/>
<classes>
<class name="com.anhtester.Bai24_Listeners.testcases.LoginTest"/>
</classes>
</test>
</suite>
Cách này sẽ có thể giúp chạy được tuỳ biến Listeners cho từng chiến lược chạy test khác nhau trên các nền tảng Web + API + Mobile, có thể cần tách ra cho từng môi trường test chẳng hạn.
Còn nếu chỉ chạy với một Listener thôi thì dùng cách setup tại BaseTest class sẽ hay nhất.