1. Mở đầu
Có lẽ các bạn không còn quá lạ lẫm với việc test code nữa. Trước đây mình nghĩ test là công việc của bên QA, và nghĩ đơn giản code mình chạy lên sau đó họ tìm một số ngoại lệ, hoặc một số chức năng chạy sai với dự tính ban đầu đó gọi là test. Nhưng khi đi làm rồi mình mới biết sau mỗi dòng code của bạn viết ra nó phải đảm bảo một số tính chất, và sẽ phải đảm bảo một số một số yêu cầu khắt khe bắt buộc, nếu bạn không muốn vào một ngày đẹp trời nó lăn ra chết và bạn tìm mỏi mắt không biết tại sao. Hay phát triển một tính năng code cũ và không hiểu tại sao nó báo bugs. Để khắc phục tình trạng này laravel đã cung cấp cho chúng ta có 2 loại Testing là UnitTest và FeatureTest. Bài viết này mình sẽ tập trung vào những khái niệm và công việc đơn giản nhất mà UnitTest thực hiện.
2. UnitTest là gì
PHPUnit là gì: Các bạn có thể hiểu đơn giản php unit sẽ đi soát từng dòng code của các bạn, từng phần gọi repositories … để kiểm tra chắc chắn những gì mà bạn đang thực hiện. Nó hỗ trợ phần lớn những PHP framework bao gồm Laravel.
Ngoài việc kiểm thử chất lượng code, theo mình thấy nó còn rất tuyệt vời để các bạn kiểm soát được những gì mình đã viết, ví dụ như kiểu dữ liệu trả về, hay qua từng bước nó sẽ truy cập như thế nào và trả về những cái gì. Nó sẽ giúp bạn đào sâu hơn về code tương đối nhiều
3. Cấu trúc thư mục test
trong đây các bạn sẽ thấy tương đối nhiều file nhưng các bạn chỉ cần quan tâm đến những phần chính:
1: tests/Feature chứa code sử dụng cho FeatureTest
2: tests/Unit chứa code sử dụng cho UnitTest
3: TestCase: là 1 bootstrap file để thiết lập môi trường Laravel cho các tests
4: phpunit.xml là file cấu hình cho PHPUnit
4. Cú pháp để tạo một test
Để tạo 1 test, ta sử dụng câu lệnh:
1 2 3 4 5 | // Tạo 1 test trong thư mục Feature php artisan make:test UserTest // Tạo 1 test trong thư mục Unit php artisan make:test UserTest --unit |
5. Một số ví dụ test
mình đang sử dụng packit, để hỗ trợ viết test
1 2 | <span class="token keyword">use</span> <span class="token package">TestsTestCase</span><span class="token punctuation">;</span> |
Để run đoạn test bạn vừa viết các bạn có thể chạy lệnh
1 2 | vendor<span class="token operator">/</span>bin<span class="token operator">/</span>phpunit <span class="token operator">--</span>filter<span class="token operator">=</span>test_it_can_show <span class="token comment">//với filte để chạy function mình đang test</span> |
Các bạn để ý đến dòng:
1 2 | <span class="token variable">$response</span> <span class="token operator">=</span> <span class="token variable">$this</span><span class="token operator">-</span><span class="token operator">></span><span class="token property">controllerMock</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">index</span><span class="token punctuation">(</span><span class="token variable">$request</span><span class="token punctuation">)</span><span class="token punctuation">;</span> |
Nó sẽ trỏ đến chính xác hàm mà các bạn đang thực thi test. Nếu không có phần này các bạn sẽ không thể test được đúng phần mình cần.
Với một controller bình thường các bạn sẽ thấy có phần construct vậy làm sao để test đoạn này:
1 2 3 4 5 6 7 | <span class="token keyword">protected</span> <span class="token variable">$mediaRepository</span><span class="token punctuation">;</span> <span class="token keyword">public</span> <span class="token keyword">function</span> <span class="token function">__construct</span><span class="token punctuation">(</span>MediaRepositoryIf <span class="token variable">$mediaRepository</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token variable">$this</span><span class="token operator">-</span><span class="token operator">></span><span class="token property">mediaRepository</span> <span class="token operator">=</span> <span class="token variable">$mediaRepository</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> |
với đoạn code trên mình sẽ viết test như sau:
1 2 3 4 5 6 7 8 9 10 11 | <span class="token keyword">protected</span> <span class="token variable">$mediaRepositoryTest</span><span class="token punctuation">;</span> <span class="token keyword">protected</span> <span class="token variable">$controllerMock</span><span class="token punctuation">;</span> <span class="token keyword">public</span> <span class="token keyword">function</span> <span class="token function">setUp</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span> void <span class="token punctuation">{</span> <span class="token variable">$this</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">afterApplicationCreated</span><span class="token punctuation">(</span><span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token variable">$this</span><span class="token operator">-</span><span class="token operator">></span><span class="token property">mediaRepositoryTest</span> <span class="token operator">=</span> Mockery<span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token function">mock</span><span class="token punctuation">(</span>MediaRepositoryIf<span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token keyword">class</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token variable">$this</span><span class="token operator">-</span><span class="token operator">></span><span class="token property">controllerMock</span> <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">UploadMediaController</span><span class="token punctuation">(</span><span class="token variable">$this</span><span class="token operator">-</span><span class="token operator">></span><span class="token property">mediaRepositoryTest</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">parent</span><span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token function">setUp</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> |
các bạn có thể hiểu đơn giản ở $this->afterApplicationCreated
đang gọi controller và setup các biến toàn cục hoặc, khởi tạo một số biến mà các bạn thấy lặp đi lặp lại
Ví dụ: vì mình check quyền policies, middleware vì vậy mỗi lần muốn truy cập vào một controller mình sẽ cần phải đăng nhập. thay vì với mỗi một function test mình là phải gõ lại đoạn đăng nhập thì mình có thể viết thẳng trên $this->afterApplicationCreated
một lần và sử dụng luôn được như sau.
1 2 3 4 5 | <span class="token variable">$user</span> <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">User</span><span class="token punctuation">;</span> <span class="token variable">$user</span><span class="token operator">-</span><span class="token operator">></span><span class="token property">id</span> <span class="token operator">=</span> <span class="token number">1</span><span class="token punctuation">;</span> <span class="token variable">$user</span><span class="token operator">-</span><span class="token operator">></span><span class="token property">role</span> <span class="token operator">=</span> <span class="token single-quoted-string string">'admin'</span><span class="token punctuation">;</span> <span class="token variable">$this</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">actingAs</span><span class="token punctuation">(</span><span class="token variable">$user</span><span class="token punctuation">)</span><span class="token punctuation">;</span> |
Mình nghĩ thường với các controller bình thường các bạn chắc hẳn sẽ có các request đầu vào, vậy làm sao để test các request này:
1 2 3 | <span class="token variable">$request</span> <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Request</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token variable">$request</span><span class="token operator">-</span><span class="token operator">></span><span class="token property">headers</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">set</span><span class="token punctuation">(</span><span class="token single-quoted-string string">'content-type'</span><span class="token punctuation">,</span> <span class="token single-quoted-string string">'application/json'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> |
trong trường hợp bạn có thêm các param truyền vào thì có thể thêm đoạn sau:
1 2 3 4 5 | <span class="token variable">$request</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">initialize</span><span class="token punctuation">(</span><span class="token punctuation">[</span> <span class="token single-quoted-string string">'user_id'</span> <span class="token operator">=</span><span class="token operator">></span> <span class="token number">1</span><span class="token punctuation">,</span> <span class="token single-quoted-string string">'role_id'</span> <span class="token operator">=</span><span class="token operator">></span> <span class="token number">1</span><span class="token punctuation">,</span> <span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span> |
Ok giời đến một đoạn test controller bình thường:
1 2 3 4 5 6 7 8 9 10 | <span class="token keyword">public</span> <span class="token keyword">function</span> <span class="token function">index</span><span class="token punctuation">(</span>Request <span class="token variable">$request</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token variable">$user</span> <span class="token operator">=</span> <span class="token variable">$request</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">user</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token variable">$media</span> <span class="token operator">=</span> <span class="token variable">$user</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">media</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">get</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">return</span> <span class="token function">response</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">[</span> <span class="token single-quoted-string string">'data'</span> <span class="token operator">=</span><span class="token operator">></span> <span class="token variable">$media</span><span class="token punctuation">,</span> <span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> |
với đoạn code đơn giản ở phái trên mình sẽ có đoạn test sau:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <span class="token keyword">use</span> <span class="token package">Mockery</span><span class="token punctuation">;</span> <span class="token keyword">public</span> <span class="token keyword">function</span> <span class="token function">test_it_can_index_upload_media</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token variable">$request</span> <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Request</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token variable">$request</span><span class="token operator">-</span><span class="token operator">></span><span class="token property">headers</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">set</span><span class="token punctuation">(</span><span class="token single-quoted-string string">'content-type'</span><span class="token punctuation">,</span> <span class="token single-quoted-string string">'application/json'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token variable">$hasManyMock</span> <span class="token operator">=</span> Mockery<span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token function">mock</span><span class="token punctuation">(</span>HasMany<span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token keyword">class</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token variable">$userMock</span> <span class="token operator">=</span> Mockery<span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token function">mock</span><span class="token punctuation">(</span>User<span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token keyword">class</span><span class="token punctuation">)</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">makePartial</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token variable">$request</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">setUserResolver</span><span class="token punctuation">(</span><span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">use</span> <span class="token punctuation">(</span><span class="token variable">$userMock</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token variable">$userMock</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token variable">$userMock</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">shouldReceive</span><span class="token punctuation">(</span><span class="token single-quoted-string string">'media'</span><span class="token punctuation">)</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">andReturn</span><span class="token punctuation">(</span><span class="token variable">$hasManyMock</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token variable">$hasManyMock</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">shouldReceive</span><span class="token punctuation">(</span><span class="token single-quoted-string string">'get'</span><span class="token punctuation">)</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">andReturn</span><span class="token punctuation">(</span><span class="token function">response</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token number">200</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token variable">$response</span> <span class="token operator">=</span> <span class="token variable">$this</span><span class="token operator">-</span><span class="token operator">></span><span class="token property">controllerMock</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">index</span><span class="token punctuation">(</span><span class="token variable">$request</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token variable">$this</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">assertInstanceOf</span><span class="token punctuation">(</span>JsonResponse<span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token keyword">class</span><span class="token punctuation">,</span> <span class="token variable">$response</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> |
Bây giời mình sẽ giải thích để các bạn có thể hiểu đoạn code trên đang chạy như thế nào:
với đoạn:
1 2 | <span class="token variable">$user</span> <span class="token operator">=</span> <span class="token variable">$request</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">user</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> |
có nghĩa là request của bạn đã đăng nhập và chưa thông tin của user. Để test đoạn trên mình có đoạn code sau:
1 2 3 4 | <span class="token variable">$request</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">setUserResolver</span><span class="token punctuation">(</span><span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">use</span> <span class="token punctuation">(</span><span class="token variable">$userMock</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token variable">$userMock</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> |
tiếp theo mình có đoạn
1 2 | <span class="token variable">$user</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">media</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">get</span><span class="token punctuation">(</span><span class="token punctuation">)</span> |
vậy $user->media()
này sẽ trả về quan hệ relationships của Models User với Models Media, của mình là hasMany lên mình sẽ có đoạn test:
1 2 3 | <span class="token variable">$hasManyMock</span> <span class="token operator">=</span> Mockery<span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token function">mock</span><span class="token punctuation">(</span>HasMany<span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token keyword">class</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token variable">$userMock</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">shouldReceive</span><span class="token punctuation">(</span><span class="token single-quoted-string string">'media'</span><span class="token punctuation">)</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">andReturn</span><span class="token punctuation">(</span><span class="token variable">$hasManyMock</span><span class="token punctuation">)</span><span class="token punctuation">;</span> |
sau đó ->get()
thì nó sẽ trả về một list trong bảng media thỏa mãn điều kiện, vì vậy mình sẽ trả về một mảng data. Nhưng các bạn có thể thấy code của chúng ta đang mong muốn trả về
1 2 3 4 | <span class="token function">response</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">[</span> <span class="token single-quoted-string string">'data'</span> <span class="token operator">=</span><span class="token operator">></span> <span class="token variable">$media</span><span class="token punctuation">,</span> <span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span> |
vì vậy mình có đoạn test:
1 2 | <span class="token variable">$hasManyMock</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">shouldReceive</span><span class="token punctuation">(</span><span class="token single-quoted-string string">'get'</span><span class="token punctuation">)</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">andReturn</span><span class="token punctuation">(</span><span class="token function">response</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token number">200</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> |
Câu cuối:
1 2 | <span class="token variable">$this</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">assertInstanceOf</span><span class="token punctuation">(</span>JsonResponse<span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token keyword">class</span><span class="token punctuation">,</span> <span class="token variable">$response</span><span class="token punctuation">)</span><span class="token punctuation">;</span> |
ám chỉ bạn đang check xem kiểu dữ liệu trả về có đúng với dữ liệu bạn cần test hay không.
6. Làm thế nào để các bạn biết UnitTest của mình đạt yêu cầu
Chúng ta viết unittest đạt yêu cầu khi đủ ba yếu tố 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
- Assert: So sánh expected với kết quả trả về.
7. Kết Luận
Đọc tới đây chắc các bạn cũng đã hình dung được unit test và một số hàm cơ bản trong unit test là gì cũng như cách sử dụng đúng không ạ. Cảm ơn bạn đã đọc bài viết của mình, nếu các bạn có thắc mắc, hay có phần nào chưa hiểu các bạn có thể bình luận dưới phần comment để chúng ta cùng tìm hiểu dõ hơn nhé. Và đặc biệt đừng quên cho mình một like và một share nhé!