Download as pdf or txt
Download as pdf or txt
You are on page 1of 17

4.3.

JUNIT VÀ MOCKITO
4.3.1. Giới thiệu JUnit
JUnit là một framework viết unit test cho ngôn ngữ Java, có vai
trò quan trọng trong phát triển các ứng dụng test-driven. Trong
tài liệu này trình bày JUnit 5, nó tương thích các phiên bản Java
8 hoặc mới hơn. JUnit 5 bao gồm ba thành phần quan trọng sau:

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

JUnit Platform định nghĩa TestEngine API để phát triển các


framework kiểm thử. JUnit Jupiter cung cấp các mô hình lập
trình mới để viết test case, bao gồm các anotation mới và hiện
thực TestEngine để chạy test case được với các annotation này.
JUnit Vintage hỗ trợ chạy các test case viết trong JUnit3 và JUnit
4 trên nền JUnit 5.
Để sử dụng JUnit 5 trên các project maven ta thêm dependency
của Jupiter Engine.
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.7.0-M1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.7.0-M1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-runner</artifactId>
<version>1.7.0-M1</version>
<scope>test</scope>
<type>jar</type>
</dependency>
4.3.2. JUnit Annotation
JUnit 5 hỗ trợ các annotation để viết test case, các annotation

155
thuộc gói org.junit.jupiter.api (Bảng 4.7).
Bảng 4.7. Các annotation để viết test case trong jUnit5.
@BeforeEach
Chạy trước mỗi phương thức test case.
@AfterEach
Chạy sau mỗi phương thức test case.
@BeforeAll
Chạy trước tất cả các phương thức test case, phương thức có
annotation phải là static.
@AfterAll
Chạy sau tất cả các phương thức test case, phương thức có
annotation phải là static.
@Test
Phương thức đóng vai trò test case.
@DisplayName
Cung cấp tên hiển thị cho phương thức test case hoặc test class.
@Disable
Bỏ qua một phương thức test case hoặc test class.
@Nested
Dùng tạo các lớp test case lồng nhau.
@Tag
Gán nhãn (tag) cho các phương thức test case hoặc test class
để dễ tìm kiếm và lọc test case.
@TestFactory
Đánh dấu phương thức là test factory cho kiểm thử động.
Ví dụ ta có lớp test như sau:
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class MyTester {


@BeforeAll
public static void setUpClass() {
System.out.println("Gọi trước tất cả TC.");
}
@AfterAll
156
public static void tearDownClass() {
System.out.println("Gọi sau tất cả TC.");
}
@BeforeEach
public void setUp() {
System.out.println("Gọi trước mỗi TC.");
}
@AfterEach
public void tearDown() {
System.out.println("Gọi sau mỗi TC.");
}
@Test
public void test1() {
System.out.println("Test case 01");
}
@Test
public void test2() {
System.out.println("Test case 02");
}
}

Hình 4.15. Minh họa sử dụng Test Annotation.


4.3.3. JUnit Assertion
Assertion dùng đánh giá (validate) đầu ra mong muốn (expected
output) và đầu ra thực sự (actual output) của test case. JUnit cung
cấp các phương thức assertion là các phương thức tĩnh của lớp
org.junit.jupiter.Assertions.
Bảng 4.8. Các phương thức tĩnh của org.junit.jupiter.Assertions.
assertEquals()
Kiểm tra hai giá trị bằng nhau.
assertNotEquals()
Kiểm tra hai giá trị không bằng nhau.

157
assertFalse()
Kiểm tra một biểu thức điều kiện là false.
assertTrue()
Kiểm tra một biểu thức điều kiện là true.
assertNotNull()
Kiểm tra một đối tượng khác null.
assertNull()
Kiểm tra một đối tượng là null.
assertSame()
Kiểm tra hai biến tham chiếu tham chiếu đến cùng đối tượng.
assertNotSame()
Kiểm tra 2 biến tham chiếu không tham chiếu cùng đối tượng.
assertThrows()
Kiểm tra chương trình ném ra ngoại lệ mong muốn hay không.
assertArrayEquals()
Kiểm tra hai mảng có bằng nhau không.
assertIterableEquals()
Kiểm tra 2 iterable hoàn toàn bằng nhau, trong đó các phần tử
cùng vị trí phải bằng nhau, và hai iterable không bắt buộc cùng
kiểu dữ liệu, chẳng hạn kiểm tra hai iterable khác kiểu dữ liệu
là LinkedList và ArrayList nếu có các phần tử bằng nhau tại
các vị trí thì xem là bằng nhau theo phương thức này.
assertLinesMatch()
Kiểm tra hai danh sách chuỗi khớp với nhau. Quá trình so
khớp cặp chuỗi (expected, actual) trong hai danh sách tương
ứng được thực hiện như sau:
- Kiểm tra nếu expected.equals(actual) là đúng thì
so sánh cặp tiếp theo.
- Ngược lại, expected được xem như biểu thức chính quy
và kiểm tra bằng phương thức matches(). Nếu khớp thì
so sánh cặp tiếp theo.
- Ngược lại, kiểm tra expected là chuỗi fast-forward (là
chuỗi bắt đầu và kết thúc bằng >> và chứa ít nhất 4 ký tự)
assertTimeout() và assertTimeoutPreemptively()
Kiểm tra nhiệm vụ trong test case được thực thi trong khoảng
thời gian (duration) cho phép.
158
Ví dụ ta có lớp phân số như sau cần viết các test case cho hai
phương thức rút gọn và so sánh bằng hai phân số.
public class PhanSo {
private int tuSo;
private int mauSo;
public PhanSo(int t, int m) {
if (m == 0)
throw new ArithmeticException("Mẫu#0");
this.tuSo = t;
this.mauSo = m;
}
public void rutGon() {
int u = ucln(this.tuSo, this.mauSo);
this.tuSo = this.tuSo / u;
this.mauSo = this.mauSo / u;
}

public static int ucln(int a, int b) {


while (a != b)
if (a > b)
a -= b;
else
b -= a;

return a;
}

@Override
public boolean equals(Object obj) {
PhanSo p = (PhanSo) obj;
int t1 = this.tuSo * p.mauSo;
int t2 = this.mauSo * p.tuSo;

return t1 - t2 == 0;
}

@Override
public int hashCode() {
int hash = 7;
hash = 97 * hash + this.tuSo;
hash = 97 * hash + this.mauSo;

return hash;
}
159
}

Thiết kế các test case kiểm tra phương thức tìm ước chung lớn
nhất hai số nguyên.
package com.dht.test1;

public class TestCase1 {


@Test
@Tag("important")
public void test1() {
assertEquals(5, PhanSo.ucln(15, 25));
}
@Test
@Tag("important")
public void test2() {
assertEquals(5, PhanSo.ucln(-15, -25));
}
@Test
public void test3() {
assertEquals(5, PhanSo.ucln(15, -25));
}
}

Khi thực thi ta sẽ thấy test2() và test3() sẽ bị failed do kết


quả thật sự và kết quả mong muốn khác nhau (phương thức tìm
ước chung lớn nhất có số nguyên âm chưa được xử lý).

Hình 4.16. Kết quả thực thi test case mẫu trên NetBeans IDE 11.2.

Thiết kế một số test case kiểm tra phương thức rút gọn phân số
và so sánh hai phân số.
- Phương thức test1() kiểm tra ném ngoại lệ khi truyền
mẫu số là 0.
- Phương thức test2() kiểm tra rút gọn phân số với tử số và
mẫu số là số dương.

160
- Phương thức test3() và test4() kiểm tra 2 hai phân số
bằng nhau.
- Phương thức test5() kiểm tra 2 mảng phân số bằng nhau.
- Phương thức test6() kiểm tra thực hiện rút gọn phân số
trong tối đa 2 giây.
package com.dht.test2;

public class TestCase2 {


@BeforeAll
public static void setUpClass() {
}
@AfterAll
public static void tearDownClass() {
}

@BeforeEach
public void setUp() {
}

@AfterEach
public void tearDown() {
}

@Test
@DisplayName("Kiểm tra ném ngoại lệ khi mẫu = 0")
public void test1() {
assertThrows(ArithmeticException.class, ()->{
new PhanSo(2, 0);
});
}

@Test
@Tag("important")
@DisplayName("Kiểm tra rút gọn phân số")
public void test2() {
PhanSo p = new PhanSo(8, 36);
p.rutGon();
assertEquals(p.getTuSo(), 2);
assertEquals(p.getMauSo(), 9);
}

@Test

161
@Tag("important")
@DisplayName("Kiểm tra hai phân số bằng nhau")
public void test3() {
PhanSo p1 = new PhanSo(-1, 2);
PhanSo p2 = new PhanSo(4, -8);
assertTrue(p1.equals(p2));
}

@Test
@DisplayName("Kiểm tra 2 phân số khác nhau")
public void test4() {
PhanSo p1 = new PhanSo(-1, 2);
PhanSo p2 = new PhanSo(-4, -8);
assertFalse(p1.equals(p2));
}

@Test
@DisplayName("Kiểm tra hai mảng phân số")
public void test5() {
PhanSo[] p1 = {new PhanSo(2, -4),
new PhanSo(8, 28)};
PhanSo[] p2 = {new PhanSo(-1, 2),
new PhanSo(-2, -7)};
assertArrayEquals(p1, p2);
}

@Test
@Tag("important")
@DisplayName("Lặp vô hạn khi tử hoặc mẫu âm")
public void test6() {
assertTimeoutPreemptively(
Duration.ofSeconds(2), () -> {
PhanSo p1 = new PhanSo(-8, 36);
p1.rutGon();
});
}
}

Phương thức test case thứ 6 sẽ bị failed do truyền tử số là số âm,


phương thức tìm ước chung lớn nhất hiện tại sẽ rơi vào vòng lặp
vô hạn nên vượt quá thời gian timout cho phép.

162
Hình 4.17. Kết quả thực thi test case PhanSo.

Assumptions cung cấp các phương thức tĩnh giúp thực thi kiểm
tra biểu thức điều kiện nào đó, khi assumptions trả về kết quả
kiểm tra thất bại thì ngoại lệ TestAbortedException sẽ được ném
ra và phương thức test sẽ dừng lại.
4.3.4. JUnit Test Suite
Test Suite cho phép thực thi test case thuộc nhiều test class và
nhiều package khác nhau. JUnit 5 cung cấp các annotation hỗ trợ
thực thi test suite.
Bảng 4.9. Các annotation hỗ trợ thực thi test suite.
@SelectPackages
Chỉ định tên các gói được chọn chạy trong test suite thông qua
@RunWith(JUnitPlatform.class).
@SelectClasses
Chỉ định tên các lớp được chọn chạy trong test suite thông qua
@RunWith(JUnitPlatform.class).
@IncludePackages và @ExcludePackages
Annotation @SelectPackages sẽ tìm test class trong tất cả các
gói chỉ định và các gói con của nó, nếu
- Nếu chỉ muốn sử dụng vài gói con trong gói chỉ định sử
dụng @IncludePackages.
- Nếu muốn bỏ qua vài gói con trong gói chỉ định sử dụng
@ExcludePackages.
@IncludeClassNamePatterns và
@ExcludeClassNamePatterns
Chỉ định các lớp được sử dụng hoặc bỏ qua khi thực thi test
suite bằng mẫu biểu thức chính quy.
@IncludeTags và @ExcludeTags
Chỉ định các phương thức test case được sử dụng hoặc bỏ qua
163
khi thực thi test suite sử dụng tag.
Ví dụ
import org.junit.platform.runner.JUnitPlatform;
import org.junit.platform.suite.api.SelectClasses;
import org.junit.platform.suite.api.SelectPackages;
import org.junit.runner.RunWith;

@RunWith(JUnitPlatform.class)
@SelectPackages({"com.dht.test2", "com.dht.test3"})
@SelectClasses(com.dht.test1.TestCase1.class)
public class TestCase {

}
4.3.5. Parameterized Test
Paramaterized Test cung cấp cơ chế thực thi một phương thức
test nhiều lần với các tham số khác nhau.
Các cách thức truyền đối số cho phương thức test.
- @ValueSource: chỉ định mảng giá trị là danh sách các đối
số lần lượt được truyền phương thức kiểm thử.
- @MethodSource: chỉ định các phương thức của lớp kiểm
thử hoặc từ lớp ngoài, phương thức này phải là phương thức
tĩnh (static).
- @CsvSource: chỉ định danh sách các phần tử, mỗi phần tử
là chuỗi gồm nhiều giá trị cách nhau bằng dấu phẩy tương
ứng là các đối số truyền vào phương thức kiểm thử.
- @CsvFileSource: đọc các đối số từ tập tin CSV, mỗi cột
của từng dòng tương ứng là danh sách các đối số truyền vào
phương thức kiểm thử.
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import static
org.junit.jupiter.params.provider.Arguments.arguments;
import
org.junit.jupiter.params.provider.CsvFileSource;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.MethodSource;

164
import org.junit.jupiter.params.provider.ValueSource;

public class TestCase3 {


@ParameterizedTest
@ValueSource(ints = {2, 3, 5, 7, 11, 13})
public void testNguyenTo(int n) {
assertTrue(Tester.ktNguyenTo(n));
}

@ParameterizedTest
@ValueSource(ints = {1, 4, 6, 10, 15})
public void testKhongNguyenTo(int n) {
assertFalse(Tester.ktNguyenTo(n));
}

@ParameterizedTest
@CsvSource({"2,true", "4,false", "7,true"})
public void testCSV(int n, boolean expected) {
assertEquals(Tester.ktNguyenTo(n),
expected);
}

@ParameterizedTest
@CsvFileSource(resources = "/data/data.csv",
numLinesToSkip = 1)
public void testCSVFile(int n,
boolean expected) {
assertEquals(Tester.ktNguyenTo(n),
expected);
}

@ParameterizedTest
@MethodSource(value = "primeData")
public void testMethod(int n,
boolean expected) {
assertEquals(Tester.ktNguyenTo(n),
expected);
}

static Stream<Arguments> primeData() {


return Stream.of(
arguments(4, false),
arguments(6, true)
);
}
}

165
Trong đó tập tin data.csv nằm trong thư mục test/resources/data
(xem Hình 4.9).

Hình 4.18. Kết quả thực thi Parameterized Test.

Hình 4.19. Minh hoạ cấu trúc project kiểm thử với junit.
4.3.6. Mockito
Unit test thực hiện việc kiểm thử các chức năng độc lập. Tuy
nhiên thường một đơn vị (lớp, phương thức) kiểm thử sẽ phụ

166
thuộc một số lớp khác, vấn đề này có thể hạn chế bằng cách sử
dụng các thành phần giả lập trong quá trình kiểm thử, điều này
giúp tập trung kiểm thử chức năng đang thực hiện.
Mocking là cách thức kiểm thử các chức năng của lớp riêng biệt,
các đối tượng mock (mock object) là các đối tượng giả lập bắt
chước các hành vi của đối tượng thật, các đối tượng này sẽ trả về
một dữ liệu giả (dummy data) tương ứng với dữ liệu vào.
Mokito là một mocking framework của Java, Mockito sử dụng
các mock interface, các chức năng giả (dummy function) sẽ được
thêm vào interface để thực hiện unit test.
Để sử dụng Mockito, ta thêm dependency sau:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.5.7</version>
<scope>test</scope>
</dependency>

Giả sử ta có lớp CalculatorApp cần kiểm thử.


class CalculatorApp {
private ICalculatorService service;

public int add(int a, int b) {


return service.add(a, b);
}

public int substract(int a, int b) {


return service.substract(a, b);
}

public void setService(ICalculatorService s) {


this.service = s;
}
}

Trong đó, interface sau chưa được hiện thực.


interface ICalculatorService {
int add(int a, int b);
int substract(int a, int b);
167
}

Để chạy lớp CalculatorApp và các phương thức của nó, ta cần


phải có lớp hiện thực của interface ICalculatorService. Trong ví
dụ này, ta sử dụng các mock object giả lập cho các thể hiện của
giao diện này để thực hiện kiểm thử.
Lớp test case kiểm thử lớp CalculatorApp.
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;

public class MokitoTestCase {


private ICalculatorService service;
private CalculatorApp app = new CalculatorApp();

@BeforeEach
public void setUp() {
this.service = mock(ICalculatorService.class);
this.app.setService(service);
}

@Test
public void testAdd() {
when(service.add(5, 20)).thenReturn(25);
Assertions.assertEquals(app.add(5, 20), 25);
}

@Test
public void testSubstract() {
when(service.substract(20, 15)).thenReturn(5);
Assertions.assertEquals(
app.substract(20, 15), 5);
}
}

Trong ví dụ này, ta tạo một đối tượng giả (mock object) cho thể
hiện của interface ICalculatorService và gắn nó vào đối tượng
cần kiểm thử là app.
Mockito cung cấp phương thức verify() để kiểm tra một
phương thức của mock object với các đối số bắt buộc có được gọi

168
không, chẳng hạn test case:
@Test
public void testAdd() {
when(service.add(5, 20)).thenReturn(25);
Assertions.assertEquals(app.add(5, 20), 25);

verify(service).add(5, 21);
}

Mockito cũng cung cấp phương thức spy() để gắn kết mock
object với đối tượng thực sự. Giả sử ta đã có một lớp hiện thực
interface ICalculatorService:
class Cal implements ICalculatorService {
@Override
public int add(int a, int b) {
return a + b;
}
@Override
public int substract(int a, int b) {
return a - b;
}
}

Ta có thể sử dụng phương thức spy() như sau:


public class MokitoTestCase {
private ICalculatorService service;
private CalculatorApp app = new CalculatorApp();

@BeforeEach
public void init() {
this.service = spy(new Cal());
this.app.setService(service);
169
}

@Test
public void testAdd() {
Assertions.assertEquals(app.add(5, 20), 25);
}
}

Ta có thể sử dụng các annotation thay thế


public class MokitoTestCase {
@Mock
private ICalculatorService service;
@InjectMocks
private CalculatorApp app = new CalculatorApp();

@BeforeEach
public void init() {
MockitoAnnotations.openMocks(this);
}

@Test
public void testAdd() {
when(service.add(5, 20)).thenReturn(25);
Assertions.assertEquals(app.add(5, 20), 25);
}

@Test
public void testSubstract() {
when(service.substract(20, 15)).thenReturn(5);
Assertions.assertEquals(
app.substract(20, 15), 5);
}
}

Lệnh MockitoAnnotations.openMocks(this) để bật tính


năng sử dụng annotation. Ta có thể khai báo annotation ở đầu
lớp thay thế: @RunWith(MockitoJUnitRunner.class).
@Mock dùng tạo mock object mà không cần gọi tường minh
phương thức mock(), trong ví dụ này tạo mock object của
interface ICalculatorService.
@InjectMocks sẽ tự động gắn các trường giả (mock field) vào
đối tượng cần kiềm thử, trong ví dụ này gắn đối tượng service
170
vào trường service của đối tượng cần kiểm thử là app.
Mockito thêm xử lý cho các chức năng vào mock object bằng
phương thức when(), chẳng hạn dòng lệnh sau thêm xử lý cho
phương thức add(), thực hiện cộng 5 và 20, kết quả là 25.
when(service.add(5, 20)).thenReturn(25)

171

You might also like