Bách khoa toàn thư về Test trong Flutter. Tập 5: Mocktail.

NALSengineering
6 min readJan 26, 2024
Source: Unsplash

Credit: Nguyễn Thành Minh (Mobile Developer)

Mở đầu

Trong những bài trước, chúng ta đã biết ứng dụng thư viện mocktail để mocking và stubbing trong Unit Test. Trong bài này, chúng ta sẽ tiếp tục cùng nhau tìm hiểu những ứng dụng khác từ thư viện mocktail.

Các hàm verify

Chúng ta cũng biết Unit Test thường test output dựa trên input của một hàm, vậy nếu như hàm đó không trả về output (void methods) thì làm thế nào để test?

Mocktail cung cấp cho chúng ta hàm verify để chúng ta kiểm tra xem một method đã được gọi hay không và được gọi bao nhiêu lần.

Giả sử chúng ta có hàm login, sau khi login thành công thì điều hướng đến màn Home.

class LoginViewModel {
final Navigator navigator;

LoginViewModel({
required this.navigator,
});

void login(String email) {
if (email.isNotEmpty) {
navigator.push('home');
}
}
}

Như vậy, để đảo bảo hàm login trên hoạt động chính xác, chúng ta cần test 2 case là:

  • Email rỗng thì navigator sẽ không gọi hàm push
  • Email khác rỗng thì navigator sẽ gọi hàm push 1 lần duy nhất

Với case đầu tiên, để assert navigator không gọi hàm push, ta sử dụng hàm verifyNever().

test('navigator.push should not be called when the email is empty', () {
// Arrange
String email = '';

// Act
loginViewModel.login(email);

// Assert
verifyNever(() => mockNavigator.push('home'));
});

Với case thứ hai, để assert navigator gọi hàm push đúng 1 lần, ta sử dụng hàm verify().called(số lần gọi).

verify(() => mockNavigator.push('home')).called(1);

Full source code

Giả sử chúng ta điều chỉnh code hàm login để khi login thành công thì ngoài push đến màn Home, nó sẽ tiếp tục push đến màn Profile.

void login(String email) {
if (email.isNotEmpty) {
navigator.push('home');
navigator.push('profile');
}
}

Khi đó, có thể chúng ta sẽ điều chỉnh lại test:

verify(() => mockNavigator.push('home')).called(2);

Nhưng khi chạy, chúng ta sẽ gặp lỗi

Thực tế hàm push chỉ được gọi đúng 1 lần? Không phải, chính xác là hàm push với argument ‘home’ chứ hàm push thì vẫn được gọi 2 lần. Trong hàm verify ở trên, do chúng ta đã truyền cụ thể arg ‘home’. Có hai cách fix lỗi này:

Cách thứ nhất là chúng ta sẽ gọi hàm verify 2 lần thay vì 1 lần:

verify(() => mockNavigator.push('home')).called(1);
verify(() => mockNavigator.push('profile')).called(1);

Cách thứ 2 ngắn gọn hơn, chúng ta chỉ cần truyền any() vào hàm push:

verify(() => mockNavigator.push(any())).called(2);

Hàm any

Hàm any được sử dụng để khớp với bất kỳ giá trị nào của một kiểu dữ liệu cụ thể. Như trong ví dụ trên, any() nó có thể khớp với ‘home’ và cũng có thể khớp với ‘profile’.

Trong 2 cách fix ở trên, nếu chúng ta truyền vào một giá trị cụ thể như ‘home’, ‘profile’ thì bài test sẽ càng chặt chẽ và càng chính xác hơn so với truyền vào any()any() là khớp với 1 giá trị bất kỳ nên giá trị đó có thể là ‘home’, ‘profile’ hoặc một giá trị khác như ‘login’, ‘register’.

Để hàm any() chặt chẽ hơn thì ta cần sử dụng đến param Matcher? that của nó. Param that được dùng để khớp với bất kỳ argument nào thoả mãn một điều kiện cụ thể.

verify(
() => mockNavigator.push(
// khớp với 1 giá trị bất kỳ kiểu String và khác rỗng
any(that: isA<String>().having((e) => e.isNotEmpty, 'isNotEmpty', true)),
),
).called(2);

Giả sử, bây giờ chúng ta đổi hàm push sử dụng named parameter như này:

void push({
required String screenName,
}) {}

Sau đó, chúng ta cần sửa lại test như này:

verify(() => mockNavigator.push(screenName: any())).called(2);

Khi chạy lại test, ta sẽ gặp lỗi:

Đó là vì khi sử dụng any() là argument cho các named parameter thì ta phải truyền tên của param đó vào param named của hàm any(). Cụ thể, ở đây ta phải sử dụng any(named: ‘screenName’) thay vì any().

verify(() => mockNavigator.push(screenName: any(named: 'screenName'))).called(2);

Chú ý: chúng ta chỉ được sử dụng hàm any() làm argument cho các method stubbing như when hoặc verification như hàm verify, nếu không thì chúng ta sẽ gặp lỗi sau:

Invalid argument(s): The "any" argument matcher is used outside of method 
stubbing (via `when`) or verification (via `verify` or `untilCalled`).

Ví dụ, khi chúng ta sử dụng nó trong hàm login, nó sẽ lỗi

loginViewModel.login(any());

Hàm captureAny

Giả sử nếu chúng ta muốn kiểm tra rằng hàm push được gọi với các argument theo thứ tự lần lượt là ‘home’ trước và ‘profile’ sau thì sao? Khi đó, nếu chỉ gọi hàm verify 2 lần như này sẽ không thể xác minh được vì không có gì đảm bảo push(‘home’) được gọi trước push(‘profile’).

// BAD
verify(() => mockNavigator.push('home')).called(1);
verify(() => mockNavigator.push('profile')).called(1);

Trong trường hợp này, chúng ta cần sử dụng đến hàm captureAny

test('navigator.push should be called with the correct argument when the email is not empty', () {
// Arrange
String email = 'ntminh@gmail.com';

// Act
loginViewModel.login(email);

// Verify that the navigator.push method is called with the correct argument
final capturedArguments = verify(() => mockNavigator.push(captureAny())).captured;
expect(capturedArguments, ['home', 'profile']);
expect(capturedArguments[0], 'home');
expect(capturedArguments[1], 'profile');
});

Hàm captureAny dùng để capture tất cả các giá trị của các argument. Sau khi capture, chúng ta có thể dùng nó để kiểm tra xem hàm đã truyền đúng argument hay không.

Tương tự như hàm any(), hàm captureAny() cũng có 2 param là namedthat với công dụng giống như hàm any().

Hàm registerFallbackValue

Giả sử bây giờ chúng ta ta update lại hàm push, thay vì truyền vào String, ta sẽ truyền vào 1 custom type là Screen

class Navigator {
void push(Screen name) {}
}

class Screen {
final String name;

Screen(this.name);
}

Tiếp theo, ở trong test ta sẽ sử dụng hàm any() để đại diện cho 1 giá trị bất kỳ có kiểu Screen

verify(() => mockNavigator.push(any())).called(2);

Khi chạy lại test, chúng ta sẽ gặp lỗi này

Họ đã hướng dẫn chúng ta cách fix bằng cách sử dụng hàm registerFallbackValue truyền vào 1 object kiểu Screen.

setUpAll(() {
registerFallbackValue(Screen('login'));
});

Tại sao hàm any() lại lỗi khi đại diện cho một custom type và hàm registerFallbackValue là gì?

Khi sử dụng các hàm any()captureAny(), mocktail cần đăng ký các default fallback value. Đối với các kiểu dữ liệu nguyên thủy, mocktail đã làm nó tự động cho chúng ta, tuy nhiên, đối với các custom type thì chúng phải sử dụng registerFallbackValue() để đăng ký các default fallback value. Nếu chúng ta không đăng ký thì sẽ dẫn đến lỗi trên.

Chúng ta chỉ cần gọi registerFallbackValue() một lần duy nhất cho mỗi type để đăng ký giá trị mặc định cho type đó và giá trị này sẽ được dùng chung cho tất cả test case, vì vậy tốt nhất chúng ta nên gọi hàm registerFallbackValue() trong setUpAll().

Full source code

Kết luận

Trong bài hôm nay, chúng ta đã cùng nhau tìm hiểu các phần nâng cao hơn của package mocktail. Trong bài tiếp theo, mình sẽ giới thiệu một kỹ thuật mới tương tự như Mocking là Faking.

--

--

NALSengineering

Knowledge sharing empowers digital transformation and self-development in the daily practices of software development at NAL Solutions.