+11

Tản mạn về Testing

Bài viết này được viết từ những ngày nắng 40 độ C, dẫn nguồn từ blog của mình, với mong muốn chia sẻ một góc nhìn cá nhân. Đây chắc là lần đầu tiên mình viết blog, sẽ có nhiều thiếu sót, rất mong nhận được sự góp ý và phản biện của các bạn:

Tôi đang làm việc cho một công ty có định hướng là một công ty công nghệ. Gần đây trong công ty tôi talk khá nhiều về Automation Test, TDD, BDD, blah blah như một mốt thời thượng, tuy nhiên lại không hiểu bản chất là gì, áp dụng thực tế như thế nào. Tôi nhận ra rằng tuyệt đại đa số nhân sự cốt lõi công ty tôi đều đang mắc kẹt trong “Hazard zone”, thích google rồi chém gió như những vị thần hơn là thực sự học hỏi, phát triển chuyên môn để nâng cao chất lượng sản phẩm. Điều này đem lại cảm giác giống như việc tôi vẫn đang tự hỏi làm sao để Việt Nam mình có thể “đón nhận và đi đầu cách mạng công nghiệp 4.0” với một nền giáo dục tệ hại như hiện tại.

Thay vì tập trung vào các mỹ từ, tôi nghĩ cần hiểu cái cốt lõi trước: Test. Bạn sẽ auto hay TDD cái gì khi bạn chưa thực hiện test một cách đúng đắn, T(Test) is DD (Dump and Die) chăng? Đây cũng là chủ đề bài viết này tập trung chia sẻ, trong phạm vi khi thêm mới hay thay đổi một chức năng của ứng dụng web, một case gặp quá phổ biến dù trong công ty sản phẩm hay công ty gia công phần mềm. Bài viết cũng không sử dụng các khái niệm hàn lâm, mà tập trung vào sự vận dụng trong thực tế.

Test thực sự là phải làm gì?

Rõ ràng là khi làm một chức năng nào đó, bạn phải kiểm tra xem chức năng đó có hoạt động đúng như yêu cầu không. Thuở hồng hoang của phát triển sản phẩm, việc bạn ngồi bấm bấm nhập nhập để xác nhận, đó là test. Khi chức năng nhiều lên, bạn cần phải tài liệu hóa việc bấm bấm nhập nhập đó, ví dụ các file excel lưu các test case và kết quả, để tái sử dụng, tránh thiếu sót và cũng là để xác nhận quá trình test của bạn.

Rồi bạn nhận ra rằng, khi bạn thêm mới hay thay đổi một chức năng, để đảm bảo chất lượng sản phẩm, bạn cần thực hiện lại toàn bộ các test case chứ không chỉ test case cho chức năng đó, người ta gọi là kiểm thử hồi quy (Regression Testing). Và khi thực hiện việc này một cách đúng đắn, bạn lại tiếp tục nhận ra rằng nếu làm bằng tay, sẽ rất tốn chi phí, bạn cần một phần mềm thực hiện việc này giúp bạn. Tôi khá là tin rằng Selenium, rồi các Testing Framework xuất hiện đều theo sự tiến hóa tự nhiên này. Và khi bạn có Jenkins, Travis CI hay những phần mềm tương tự, giúp tự động hóa các quy trình trong phát triển phần mềm, khái niệm Automation Test mới ra đời.

Điểm chung của quá trình nói trên đó là việc tạo ra các test case như là output và đây cũng là thứ tạo ra giá trị gia tăng của bạn trong phần test. Nếu như trước đây, bạn chỉ phải lưu các test case vào excel và thực hiện nó một cách thủ công thì ngày nay, bạn cần phải tạo ra các TaaC (Test Case as a Code) kiểu như:

<?php

$I->visit('/login')
  ->fill('email', 'john@doe.com')
  ->fill('password', 'secret')
  ->click('Login')
  ->amOnPage('/dashboard')
  ->see('John Doe');

Nếu trong dự án của bạn, các test case đều được viết đầy đủ và cover các chức năng theo kiểu TaaC như trên thì, xin chúc mừng!

Ai sẽ phải viết test?

Câu hỏi “Dev thì có phải viết test không?” có lẽ là được đặt ra khá nhiều. Câu trả lời rất đơn giản, giống như khi bạn làm ra cái gì, bạn phải kiểm tra nó có hoạt động không, đương nhiên các developer phải viết test.

Theo tự nhiên, câu hỏi tiếp theo sẽ là “Vậy Dev sẽ viết test nào, bộ phận QA để làm gì vậy?”. Để trả lời câu hỏi này, chúng ta cần đi phân loại test.

Để phân loại, tôi nhớ đến kiến thức đã được học ở khoa CNTT, ĐHBKHN, phân loại theo phương thức tiếp cận: phương thức kiểm thử hộp đen (black-box testing) và phương thức kiểm thử hộp trắng (white-box testing).

Black-box testing, nôm na là cái hộp đen, bạn sẽ không cần quan tâm những gì xử lý trong cái hộp đen đó, chỉ cần cho đầu vào và xác nhận đầu ra, nghe khá giống với những gì với bộ phận QA độc lập thực hiện.

White-box testing, nôm na là cái hộp trắng, bạn sẽ cần quan tâm những gì xử lý trong hộp trắng đó, vì trắng nhìn xuyên qua mà. Nếu quan tâm tới cụ thể code được viết như thế nào, kiến trúc ra sao, nghe khá giống với những gì developer cần làm.

Với suy nghĩ đơn giản như vậy, tôi đã phải bật cười khi đọc trên JIRA (công ty tôi dùng JIRA bản quyền để quản lý dự án) có những task kiểu như “Viết unit test cho …” song hành cùng với “Viết automated test cho…” hay giật mình khi một “senior” 20 năm kinh nghiệm phân loại rằng bộ phận QA sẽ làm kiểm thử hộp trắng.

Để phân loại chi tiết hơn có khá nhiều cách, mỗi sách lại có một định nghĩa riêng, nói chung tất cả đều hợp lý tùy vào trường hợp cụ thể. Tuy nhiên, qua kinh nghiệm thực tế trong môi trường phát triển phần mềm ở Việt Nam, tôi ủng hộ một cách phân loại đơn giản nhưng có giá trị áp dụng thực tiễn: Black-box testing sẽ là Acceptance Testing do bộ phận QA thực hiện, White-box testing sẽ chia thành Unit Testing và Functional Testing do developer thực hiện.

Acceptance Testing

Acceptance Testing về cơ bản là mô phỏng các thao tác của người dùng sử dụng sản phẩm để xem kết quả người dùng nhận được có đúng như mong muốn không. Nó giống như việc test được mô tả ở phần đầu bài viết.

Suy cho cùng, dù bạn làm gì, mục tiêu của bạn vẫn luôn là làm hài lòng khách hàng, và tôi không nghĩ là khách hàng sẽ hài lòng với một sản phẩm chất lượng không chấp nhận được. Nếu được hỏi bắt đầu viết test từ đâu khi tiếp nhận maintain một dự án legacy, tôi sẽ trả lời là từ đây. Acceptance Testing là chốt chặn cho chất lượng sản phẩm bạn bàn giao khách hàng.

Ngày nay, thế giới đã cung cấp khá đầy đủ những công cụ “make tester’s life easier”, giúp bạn viết Acceptance Testing dễ dàng. Ví dụ tôi dùng Laravel Dusk để test chức năng cho phép người dùng đăng nhập thêm các SSH key, sau đây là hai case đơn giản cho chức năng này:

<?php

namespace Tests\Browser;

use App\User;
use Tests\DuskTestCase;
use Laravel\Dusk\Chrome;
use Illuminate\Foundation\Testing\DatabaseMigrations;

class UserKeysTest extends DuskTestCase
{
    use DatabaseMigrations;
    
    protected $user;
    
    public function setUp()
    {
        parent::setUp();
        
        $this->user = factory(User::class)->create([
            'email' => 'john@example.com',
        ]);
    }

    /**
     * Test an user can add a ssh key.
     *
     * @return void
     */
    public function test_an_user_can_add_a_ssh_keys()
    {   
        $this->browse(function ($browser) {
            $browser->loginAs($this->user)
                ->visit('/user/keys/create')
                ->type('name', 'Macbook')
                ->type('public_key', 'ssh-rsa xxx')
                ->press('Create')
                ->assertPathIs('/user/keys')
                ->assertSee('Macbook');
        });
    }
    
    /**
     * Test name is required.
     *
     * @return void
     */
    public function test_name_is_required()
    {
        $this->browse(function ($browser) {
            $browser->loginAs($this->user)
                ->visit('/user/keys/create')
                ->type('public_key', 'ssh-rsa xxx')
                ->press('Create')
                ->assertSee('The name is required.');
        });
    }
}

Khi thực hiện Acceptance Testing, có những việc chưa hoặc khó mô tả bằng code, ví dụ kiểm tra tin nhắn OTP có được gửi về khi người dùng đăng nhập chẳng hạn, hay kiểm tra các chi tiết của giao diện sản phẩm, bộ phận QA vẫn phải thực hiện thủ công. Tên gọi User Acceptance Testing (hay Beta Testing) được sử dụng để chỉ việc kiểm thử chi tiết với vai trò người dùng cuối như vậy.

Nếu trong dự án bạn đang làm, Acceptance Testing đơn giản chỉ là vài dòng Excel, hay thậm chí là một thứ xa xỉ, tôi nghĩ là sự cải tiến là cấp bách, vì như vậy có nghĩa là bạn khó đảm bảo được chất lượng sản phẩm, và đang tụt hậu so với thế giới nhiều năm rồi.

Unit testing

Unit testing bản thân nó là cái gì đó khá trừu tượng vì tùy dự án, bạn có thể quy định “unit” ở mức độ nào. Thông thường, “unit” sẽ được quy định giới hạn trong một hàm (method) hay một class. Trong một dự án web thực tế, tùy vào kinh nghiệm và kĩ năng, developer sẽ đưa ra quyết định viết các Unit testing như thế nào cho phù hợp, có thể test đầu vào đầu ra của hàm, hay kiểm tra một phần hoặc toàn bộ class.

Một ví dụ unit testing đơn giản của bài toán Fizz buzz, cho đầu vào là 3, kiểm tra đầu ra của hàm fizzbuzz() có là fizz không:

<?php

use PHPUnit\Framework\TestCase;

class FizzBuzzTest extends TestCase
{
    // ...
    
    function test_3_is_fizz()
    {
        $this->assertEquals(fizzbuzz(3), 'fizz');
    }
    
    // ...
}

Hay trong một side project, tôi viết một unit testing thể hiện việc model User có quan hệ 1-n với model Server:

<?php

namespace Tests\Unit;

use Tests\TestCase;
use Illuminate\Foundation\Testing\DatabaseMigrations;

class UserTest extends TestCase
{
    use DatabaseMigrations;
    
    protected $user;
    
    public function setUp()
    {
        parent::setUp();
        
        $this->user = create('App\User');
    }
    
    public function test_an_user_has_servers()
    {
        $this->assertInstanceOf(
            'Illuminate\Database\Eloquent\Collection', $this->user->servers
        );
    }
}

Nếu cần thêm các ví dụ, bạn có thể tham khảo thêm Unit testing được viết trong các thư viện mã nguồn mở trên Github.

Functional Testing

Functional Testing, hay cá nhân tôi hay gọi là Feature Testing, đúng với nghĩa của nó, test chức năng với các xử lý business logic bên trong chức năng đó đồng thời cô lập (mock) với các thành phần bên ngoài khác.

Ví dụ cụ thể, trong một side project, tôi cần thêm API cho phép user thêm mới một server, luồng xử lý là như sau:

  • Đăng nhập bằng một user.
  • Gửi POST request đến URL /api/servers với payload {"name": "small-snowflake", "ip_address": "1.1.1.1"}.
  • Response trả về thành công (có HTTP code là 200).
  • Đọc public key của bản thân web server.
  • Thêm mới bản ghi vào bảng servers, lưu thông tin server và public key vừa đọc được.
  • Gọi queue job AddServerToKnownHostsFile (để thêm địa chỉ IP của server vào known_hosts file).
  • Fire sự kiện RefreshServers.

Đọc public key của bản thân web server là hành động tương tác với bên ngoài, cái tôi thực sự cần chỉ là public key để lưu vào DB, cho nên tôi sẽ cô lập việc đọc này, giả định nó chạy đúng và trả về chuỗi public key.

Gọi queue job AddServerToKnownHostsFile là một chức năng bên ngoài, tôi giả định là nó chạy đúng và chỉ cần quan tâm là job này có được gọi hay không. Tương tự với fire sự kiện RefreshServers. Bản thân job AddServerToKnownHostsFile hay sự kiện RefreshServers có thể được test trong các unit testing hay functional testing độc lập khác.

Tôi có thể viết functional testing của chức năng này như sau:

<?php

namespace Tests\Feature;

use Tests\TestCase;
use App\Events\RefreshServers;
use Illuminate\Support\Facades\File;
use App\Jobs\AddServerToKnownHostsFile;
use Illuminate\Foundation\Testing\DatabaseMigrations;

class CreateServerTest extends TestCase
{
    use DatabaseMigrations;
    
    protected $user;
    
    public function setUp()
    {
        parent::setUp();
        
        $this->user = create('App\User');
    }
    
    public function test_an_user_can_create_new_servers()
    {
        File::shouldReceive('get')
            ->once()
            ->andReturn('public-key');
        
        $this->expectsJobs(AddServerToKnownHostsFile::class);
        
        $this->expectsEvents(RefreshServers::class);
        
        $this->actingAs($this->user, 'api')
            ->postJson('/api/servers', [
                'name' => 'small-snowflake',
                'ip_address' => '1.1.1.1',
            ])
            ->assertStatus(200);
        
        $this->assertDatabaseHas('servers', [
            'user_id' => $this->user->id,
            'name' => 'small-snowflake',
            'ip_address' => '1.1.1.1',
            'public_key' => 'public-key',
        ]);
    }
}

Kết luận

Một khi dự án của bạn thực sự có các TaaC, tôi khá tin tưởng rằng năng suất của dự án tăng lên nhiều lần cả về chất lượng, chi phí, lẫn khả năng delivery. Khi bạn sử dụng Jenkins để tự động quy trình thực hiện các TaaC đó, bạn sẽ có Test Automation. Khi bạn có Test Automation, bạn sẽ có khả năng kiểm soát rủi ro khi phải giao hàng sớm và thường xuyên cho khách hàng. Và khi bạn có thể làm vậy, bạn đã có một trong những điều kiện tiên quyết để trở nên Agile, Agile thực sự chứ không phải khuôn sáo, vẽ vời lý thuyết như bán hàng đa cấp.

Về quy trình phát triển phần mềm, khi bạn có các TaaC, bạn sẽ nhận ra rằng, việc viết test trước sẽ giúp bạn có cái nhìn rõ ràng hơn về yêu cầu khách hàng, bạn có Test-Driven Development chứ không phải Test is Dump and Die nữa 😃.

Thực sự viết được các TaaC cho một dự án, không phải điều dễ dàng, nếu không muốn nói là khó, đòi hỏi hiểu biết về kiến trúc và thiết kế tổng quan, hiểu yêu cầu của sản phẩm, viết code testable, các kĩ thuật mocking. Để làm được cần kiến thức cơ bản tốt, thực hành và rèn luyện kĩ năng, với mindset tập trung vào việc làm ra những sản phẩm chất lượng, liên tục cải tiến năng suất và chứng minh điều đó qua sự hài lòng của khách hàng. Chứ đừng như một số “senior” tôi vẫn hay gặp, talk như là expert về những thứ mà chỉ biết qua google, không có kĩ năng và kinh nghiệm làm bao giờ, và khi training cho các fresher, junior thì làm liên tưởng đến những giáo viên tiếng anh ngày xưa dạy học sinh phát âm sai vì ngay bản thân mình cũng không biết phát âm thế nào. Nếu không, thì tôi không nghĩ chúng ta có thể sớm thoát khỏi được cái mác nhân công giá rẻ, mặt bằng chất lượng tệ hại và mãi là vùng trũng của phát triển phần mềm.

talk: nó mang sắc thái trong câu nói nổi tiếng của Linus Torvalds: “Talk is cheap. Show me the code”.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí