[Unit test] PHP Testing Interview

Một cuộc phỏng vấn với PHP Testing giúp mình tổng hợp các ý tưởng, cách thức về việc testing với developer!

Giới thiệu automation test, unit test và integration test

Me:: Hôm nay là ngày 11/1, một ngày không khí lạnh tràn về Hà Nội, vì vậy tôi muốn phỏng vấn bạn mấy câu có được không PHP Testing?

PHP Testing: Tất nhiên, tôi rất vui vì được bạn phỏng vấn!

Me: Tôi có nghe về Automation Tests, nó là cái gì vậy?

PHP Testing: Dịch sang tiếng Việt nó là kiểm thử tự động, hãy hiểu nó là 1 phần nhỏ trong bức tranh Test rộng. Nó chính là viết code để test code.

Me: Vậy nó đem lại lợi ích gì vậy?

PHP Testing: Nó có rất nhiều lợi ích đó (nếu phát triển theo đúng quy trình).

  • Nó cấu trúc code cho bạn (đặt tên các hàm sẵn cho bạn trong test)
  • Có thể tích hợp CI/CD để tự động hóa các quá trình merge, deploy.
  • Một điểm mạnh của nó nữa là nó có thể teamwork, nó kiểm tra được đoạn code của bạn có ảnh hưởng đến code người khác không (cái này cực kì hay gặp khi sửa 1 hàm mà làm ảnh hưởng đển code của người khác nè). Từ đó giúp bạn tăng sự tự tin trong việc thay đổi / bảo trì / refactor code =))

Me: Nghe hay quá nhỉ, vậy developer áp dụng nó vào project như thế nào?

PHP Testing: Developer sẽ áp dụng vào project dưới dạng Unit TestIntegration test

Me: Nó là cái gì thế?

PHP Testing: À cũng không khó lắm đâu. Unit Test thì đơn giản, nó sẽ Test từng function hoặc method của một class, nhận đầu vào và mong chờ (asset) đầu ra đúng như kì vọng. Nó sẽ không truy vấn database, sử dụng network, call API, sử dụng file system. Trong khi đó Integration Test là việc test tích hợp nhóm các function, các method đã kết hợp lại với nhau và thực hiện các việc mà các Unit Test không được làm ở trên.

Me: Unit Test thì tôi đã hiểu, nó test từng hàm một, nhưng Integration test thì tôi chưa rõ lắm.

PHP Testing: Tôi lấy ví dụ cho bạn nhé, Chẳng hạn Unit test, test từng method của Service và Controller sử dụng service thì Integration sẽ test việc sử dụng kết hợp service và controller thực hiện công việc gì đó như tạo bản ghi. Đầu vào sẽ là input, sau đó thực hiện Controller này asset đầu ra nó đã có trong DB chưa, nó đơn giản là hàm gọi nhiều hàm tích hợp với nhau thôi :D.

Với những ứng dụng nhỏ, số lượng code không quá nhiều, chúng ta có thể chỉ cần thực hiện integration tests mà sử dụng database connection, lợi ích là giảm effort mà vẫn giữ được tốc độ của cả quá trình testing.

Các thành phần chung của một test case

Me: Cảm ơn bạn, tôi cũng đã hiểu sơ sơ rồi. Mà trong các test bao gồm nhiều test cases phải không nhỉ, nó có phải là các trường hợp kiểm thử, và khi viết test cases đó thì tôi cần những gì?

PHP Testing: Đúng vậy các test case chính là các trường hợp chúng ta sẽ kiểm thử trong code. Các Test Cases phải được thiết kế để có thể cover được hết các sự kết hợp của các giá trị inputs cùng các điều kiện, bao phủ hết các nhánh if/else. Một Test case sẽ bao gồm 3 phần sau:

  • Arrange: thiết lập trạng thái, khởi tạo object, giả lập mock
  • Act: Chạy unit đang cần test (method under test)
  • Assert: So sánh expected với kết quả trả về
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public function test_fetches_items_in_array_until_value()
    {
    // Arrange
    $names = ['Taylor', 'Dayle', 'Matthew', 'Shawn', 'Neil'];

    // Act
    $result = array_until('Matthew', $names);

    // Assert
    $expected = ['Taylor', 'Dayle'];
    $this->assertEquals($expected, $result);
    }
    Hoặc có thể phát biểu thành lời với GIVEN, WHEN, THEN:

Given this set of data, when I perform this action, then I expect that response.

1
2
3
4
5
6
7
8
9
10
11
12
public function test_fetches_items_in_array_until_value()
{
// Given this set of data
$names = ['Taylor', 'Dayle', 'Matthew', 'Shawn', 'Neil'];

// When I call the until function and specify a value
$result = array_until('Matthew', $names);

// Then I expect the result should be
$expected = ['Taylor', 'Dayle'];
$this->assertEquals($expected, $result);
}

Testing khi chức năng phụ thuộc bên thứ 3 hoặc lớp khác

Me: Oh, tôi đã hiểu test case. Ở trên bạn có nói việc sử dụng network, gọi API làm chậm test, kết quả không ổn định vì phụ thuộc mạng, khi test truy vấn CSDL thì làm chậm quá trình test và ảnh hưởng đến database thật, vậy gặp trường hợp này thì chúng ta test kiểu gì?

PHP Testing: Test cô lập (Test Doubles)

Me: Bạn có thể nói rõ hơn không?

PHP Testing: Những phần phụ thuộc bên thứ 3 trong test sẽ được giả lập vì nếu không giả lập, khi chạy test sai ta không biết sai do code của test hay do bên thứ 3. Giả lập ở đây tức là thay thế Object hoặc hàm phụ thuộc bên thứ 3 bằng Object và hàm “bắt chước” hành vi của real Object và chúng ta có thể tự định nghĩa kết quả trả về theo từng kịch bản test case?

Me: Đúng là một ý tưởng tuyệt vời, thế lúc nào tôi cũng giả lập cả Object à?

PHP Testing: Không đâu, như tôi nói ở trên đó, bạn có thể giả lập hàm hoặc cả Object ứng với 2 quan điểm về unit test cô lập. Sociable test sẽ giả lập các hàm, các methods chậm, sức ảnh hưởng lớn hoặc không thể dùng trong môi trường test: database, network call. Và loại còn lại là Solitary test giả lập cả Object luôn, như Dependency Injection chẳng hạn.

Me: Làm sao để tôi biết khi nào sử dụng Sociable testSolitary test?

PHP Testing: Như thế này nhé, nếu cấu trúc code của bạn tốt thì:

  • Model: nhiệm vụ chính là để lưu dữ liệu bên trong và không thực hiện nhiều logic bên trong => không cần mock, chỉ đơn giản là khởi tạo đối tượng và truyền vào fake data cho nó
  • Service: nhiệm vụ chính là thực hiện công việc, logic => cần cả 2 loại trên tùy vào chức năng?

Me: Hay quá, bạn có thể cho tôi xem thực hành về việc giả lập này không?

PHP Testing: Tất nhiên rồi, giả lập này sẽ gồm 2 khái niệm và StubMock. Stub (Stub methods) là giả lập hành vi tức là các methods và functions. Bạn có thể giả lập hàm này trong 1 lần gọi (lần gọi đầu sẽ mock, từ lần sau sẽ là methods, functions cũ) hay áp dụng cho mọi lần gọi:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public function test_order_sends_mail_if_succeeded()
{
$mailService = $this->createMock(MailService::class);
$order = new Order('Wine', 'user@localhost', $mailService);

// Expect method MailService::send được gọi duy nhất 1 lần,
// với 2 tham số là 'user@localhost' và 'Order succeeded!'
// Expect cần được viết trước khi gọi method test
$mailService->expects(once())
->method('send');
->with(
$this->equalTo('user@localhost'),
$this->equalTo('Order succeeded!')
);

$result = $order->process();

$this->assertTrue($result);
}

Còn Mock thì giả lập trạng thái loại bỏ tất cả logic bên trong method của object thật và có thể thay đổi kết quả trả về của method theo ý muốn
1
2
3
4
// Create mock object
$product = $this->createMock(Product::class);
// Stub method price, make it return 100
$product->method('getPrice')->will($this->returnValue(100));

Hai khái niệm này thậy mơ hồ. Đây là một ý kiến tôi đọc được và cảm thấy dễ hiểu:

Một Stub không thể trả về kết quả unit test là fail bởi vì bạn biết những gì bạn đang thực hiện và lý do tại sao bạn đang thực hiện nó, Nó được sử dụng là để thay thế cho một module và được giả sử là phải thực hiện đúng các nhiệm vụ được giao. Tuy nhiên, Mock object chỉ là một đối tượng mà bắt chước các đối tượng thực sự. Nếu logic chính của method là sai thì các unit test sẽ fail ngay cả khi chúng ta thiết lập mock object chính xác.

Tổng hợp 1 số ý kiến trên stackoverflow

Style

Mocks vs Stubs = Behavioral testing vs State testing

Principle

According to the principle of Test only one thing per test, there may be several stubs in one test, but generally there is only one mock.

Lifecycle

Test lifecycle with stubs:

  1. Setup - Prepare object that is being tested and its stubs collaborators.
  2. Exercise - Test the functionality.
  3. Verify state - Use asserts to check object’s state.
  4. Teardown - Clean up resources.

Test lifecycle with mocks:

  1. Setup data - Prepare object that is being tested.
  2. Setup expectations - Prepare expectations in mock that is being used by primary object.
  3. Exercise - Test the functionality.
  4. Verify expectations - Verify that correct methods has been invoked in mock.
  5. Verify state - Use asserts to check object’s state.
  6. Teardown - Clean up resources.

Summary

Both mocks and stubs testing give an answer for the question: What is the result?

Testing with mocks are also interested in: How the result has been achieved?

Chính vì thay đổi Object là Object mới, rất có thể sinh ra lỗi nếu bạn mock logic của Object sai, đó có thể là khác biệt lớn nhất giữa Stub và Mock.

Một ý kiến khác về stub, mock, fake. Fake là giả, tạo một cái gì đó giả, muốn là gì thì là miễn là kiểu đó =)). Nó phục vụ việc các hành vi không mong kết quả trả về như click button, gửi mail, …

  • Stub - an object that provides predefined answers to method calls.
  • Mock - an object on which you set expectations.
  • Fake - an object with limited capabilities (for the purposes of testing), e.g. a fake web service.
    Test Double is the general term for stubs, mocks and fakes. But informally, you’ll often hear people simply call them mocks.

Me: OK phần network call, API call thì tôi đã hiểu, vậy phần database thì sao, chúng ta cũng giả lập như vậy?
PHP Testing: À với database thì không nhé. Với databse, chúng ta sẽ tạo ra một môi trường mới (thường là testing) với database mới có cấu trúc giống như database dev và tiến hành test dữ liệu trên database đó. Với các service bên ngoài ta vẫn nên giả lập. Nếu trường hợp dữ liệu cần có 1 vài dữ liệu trước đó bạn phải tự tạo. Ví dụ như bạn muốn test trường hợp xóa 1 bản ghi, thì trong bước chuẩn bị bạn cần tạo bản ghi đó đúng không nào?

Một vài notes khi thực hành test với Laravel

Me: Tôi đã hiểu các khái niệm Unit Test và thấy nó khá thú vị rồi đó, bạn có thể hướng dẫn tôi thực hành trong một framwork nào được không?

PHP Testing: Laravel nhé

Me: Hehe, tôi là một master Laravel đó :D, thực hành thôi!

PHP Testing: Trước tiên tôi muốn giới thiệu

  1. SETUP VÀ TEARDOWN dùng để thiết lập class property, object, env
  • setUp(): Chạy trước mỗi method test
  • tearDown(): Chạy sau mỗi method test
  • setUpBeforeClass(): Chạy khi bắt đầu class test
  • tearDownAfterClass(): Chạy sau khi kết thúc class test
  1. Tiếp đó là REFLECTION

Trong một số trường hợp chúng ta phải truy cập vào private/protected method hoặc lấy ra các private/protected property của đối tượng để thực hiện assertion

  1. Service Container

Đây chính là nơi bạn đưa các DI vào để test thông qua các phương thức resolve

1
2
3
4
5
6
7
public function test_register_when_valid_email_password_return_success()
{
$mailService = $this->createMock(MailService::class);
// Just use fake mail service instead of SES in this test
App::instance(MailService::class, $mailService);
// ...
}

  1. @dataProvider

Tạm dịch là: 1 test method có thể chấp nhận nhiều input khác nhau. Các tham số này được cung cấp bởi method data provider.

Bạn hãy theo dõi 1 số ví dụ để hiểu rõ cách test hơn nhé!!

Me: Okay. Tôi sẽ luyện tập phần unit test này để hiểu rõ hơn. Cảm ơn bạn vì cuộc trò chuyện nhé!

Tổng kết

  • Keyword: Unit Test, Integration Test, Test Doubles, Mock Objects, Stub Methods, Fake, Dependency Injection, Mockery, REFLECTION.
  • Methods: @dataProvider, setUp(), tearDown(), setUpBeforeClass(), tearDownAfterClass().

  • Các khái niệm Unit Test tôi thấy không chỉ áp dụng cho PHP hay Laravel, nó là ý tưởng chung cho testing đối với Developer. Ví dụ khái niệm Unit Test, Test tích hợp hay Test cô lập như Stub, Mock, Fake dùng rất nhiều trong JEST để test các framework JS với ý tưởng như vậy. Thậm chí với testing cho các view bằng js, chúng ta còn có khái niệm snapshot testing cực kì hay. Cấu trúc của 1 test case cùng vậy, dù test bằng ngôn ngữ gì đi chăng nữa cũng sẽ có 3 phần GIVEN, WHEN, THEN.

  • Các hàm để test cũng vậy, Laravel có setUp(), tearDown(), setUpBeforeClass(), tearDownAfterClass(), Laravel có Mockery, các framework JS có JEST, Mocha/chai

  • Thật là tuyệt vời phải không nào, học ý tưởng cách test của 1 ngôn ngữ ta lại áp dụng được cho các ngôn ngữ khác.

Tài liệu tham khảo:

Slide:

Docs:

Demo:

Sun Asterisk Training Project:

Author

Ming

Posted on

2020-01-10

Updated on

2021-10-25

Licensed under

Comments