Javascript Testing với Jasmine
1.Giới thiệu về khóa học
Đây là khóa học miễn phí, giới thiệu về bộ công cụ mã nguồn mở Jasmine dùng để xây dựng Unit test với mã nguồn JavaScript
Nội dung của khóa học gồm các phần chính:
- Tổng quang lại về Unit test
- Giới thiệu về BDD( Behavior Driven Development)
- Javascript Testing với Jasmine
2.Tổng quan về Unit Test
Unit Test là gì ?
Unit Testing, kiểm tra từng bộ phận rất nhỏ, từng unit riêng biệt trong source code của chương trình để kiểm tra xem nó có hoạt động chính xác không.
Từ những bộ phận nhỏ này, ta lại kiểm tra những unit lớn hơn có sử dụng những unit nhỏ đã được kiểm tra đó.
Một Unit Test là một phần của source code, thực thi một phần code chính khác và so sánh kết quả thực tế với kết quả mong đợi.
Được thực hiện bởi các lập trình viên.
Có thể làm bằng tay (Manual Unit Test) hoặc tự động (Automated Unit Test)
Tầm quan trọng của Unit Test
- Đảm bảo chất lượng từng Unit trong phần mềm.
- Phát hiện lỗi sớm và chỉnh sửa kịp thời
- Giảm chi phí
- Tái sử dụng được
- Giúp chúng ta Design
Phương pháp xây dựng Unit Test
Thiết kế Unit Test theo trình tự sau :
- Thiết lập các điều kiện cần thiết: khởi tạo các đối tượng, xác định tài nguyên cần thiết, xây dựng các dữ liệu giả…
- Triệu gọi các phương thức cần kiểm tra.
- Kiểm tra sự hoạt động đúng đắn của các phương thức.
- Dọn dẹp tài nguyên sau khi kết thúc kiểm tra.
Nguyên tắc thiết kế Unit Test
- Phân tích các tình huống có thể xảy ra đối với mã.
- Mọi UT phải bắt đầu với trạng thái “fail” và chuyển trạng thái “pass” sau một số thay đổi hợp lý đối với mã chính.
- Khi viết một đoạn mã quan trọng, hãy viết các UT tương ứng.
- Số lượng Test Case đủ lớn để phát hiện điểm yếu của mã theo nguyên tắc:
- Nếu nhập giá trị đầu vào hợp lệ thì kết quả trả về cũng phải hợp lệ
- Nếu nhập giá trị đầu vào không hợp lệ thì kết quả trả về phải không hợp lệ
Giới thiệu về Mock Object trong Unit Test
- Định nghĩa: Mock object (MO) là một đối tượng ảo mô phỏng các tính chất và hành vi giống hệt như đối tượng thực được truyền vào bên trong khối mã đang vận hành nhằm kiểm tra tính đúng đắn của các hoạt động bên trong.
Đặc điểm:
Đơn giản hơn đối tượng thực nhưng vẫn giữ được sự tương tác với các đối tượng khác.
Không lặp lại nội dung đối tượng thực.
Cho phép thiết lập các trạng thái riêng trợ giúp kiểm tra.
Mock Object được sử dụng khi: Có thể sử dụng “mock object” để mô phỏng các đối tượng thật(real object) sau:
Không có hành vi cụ thể => không thể đoán trước kết quả: Các đối tượng có các tính chất và hành vi phức tạp, các trạng thái luôn thay đổi và các quan hệ chặt chẽ với nhiều đối tượng khác
Khó cài đặt: Các đối tượng thực rất khó cài đặt (thí dụ đối tượng xử lý các trạng thái của server)
Xử lý chậm: Các đối tượng vận hành chậm chạp. Công việc kiểm tra hiện hành không liên quan đến thao tác xử lý đối tượng này.
Khó xảy ra và dễ gây lỗi: Các đối tượng thực xử lý một tình huống khó xảy ra. Thí dụ lỗi kết nối mạng, lỗi ổ cứng…
Object liên quan giao diện người dùng: Không người dùng nào có thể ngồi kiểm nghiệm các chức năng hộ bạn hết ngày này qua ngày khác. Tuy nhiên bạn có thể dùng MO để mô phỏng thao tác của người dùng, nhờ đó công việc có thể được diễn biến lặp lại và hoàn toàn tự động.
Object chưa tồn tại dạng mã : Các đối tượng thực mới chỉ được mô tả trên bản thiết kế nhưng chưa tồn tại dưới dạng mã, hoặc các module chưa sẵn sàng cung cấp các dữ liệu cần thiết để vận hành UT.
3.Giới thiệu BDD
BDD (Behavior Driven Development) là gì ?
- BDD (Behavior Driven Development) là một quá trình phát triển phần mềm dựa trên phương pháp Agile(phát triển phần mềm linh hoạt).
- BDD là sự mở rộng của TDD (Test driven development). Thay vì tập trung vào phát triển phần mềm theo hướng kiểm thử, BDD tập trung vào phát triển phần mềm theo hướng hành vi.
- Dựa vào các yêu cầu của phần mềm, các kịch bản test (Scenarios) sẽ được viết trước dưới dạng ngôn ngữ tự nhiên và dễ hiểu nhất sau đó mới thực hiện cài đặt source code để pass qua tất cả các kịch bản test đó.
- BDD giúp chúng ta có spec rõ ràng nên dễ dàng viết code, viết code xong thì không cần test ( vì đã viet test xong rồi).
- Viết test theo yêu cầu của khách hàng nên sẽ tránh được trường hợp test thừa hoặc thiếu.
- BDD hình thành một cách rất tự nhiên, xuất phát từ những yêu cầu.
- Người viết BDD là tất cả các thành viên trong dự án và các bên liên quan
- Quy trình phát triển phần mềm theo BDD
Những lợi ích khi sử dụng BDD
- Giúp xác định đúng yêu cầu của khách hàng: tài liệu được viết dưới dạng ngôn ngữ tự nhiên, bất kỳ đối tượng nào cũng có thể hiểu được. Khi đọc tài liệu này, khách hàng có thể dễ dàng nhận biết được lập trình viên có hiểu đúng yêu cầu của họ không và có phản hồi kịp thời.
- Là tài liệu sống của dự án: tài liệu này luôn được cập nhật khi có bất kỳ sự thay đổi nào nên tất cả các thành viên sẽ không bị miss thông tin khi phát triển hệ thống
- Nâng cao chất lượng phần mềm, tạo ra sản phẩm hữu ích: vì phát triển phần mềm theo hướng hành vi nên có thể focus vào việc tạo ra sản phẩm đúng với yêu cầu của khách hàng nhưng vẫn hữu ích cho người dùng.
Giới thiệu Cucumber
Cucumber là một công cụ kiểm thử tự động dựa trên việc thực thi các chức năng được mô tả dướng dạng plain-text, mục đích là để hỗ trợ cho việc viết BDD.
Ngôn ngữ được cucumber sử dụng là “Gherkin”
Gherkin là 1 ngôn ngữ mà Cucumber đọc ngôn ngữ ấy chuyển thành test. Gherkin khá dễ hiểu, người đọc có thể hiểu kịch bản và hành động mà không cần biết chi tiết chúng được cài đặt như thế nào
Gherkin thỏa mãn 2 mục đích- Cung cấp tư liệu
- Test tự động
Có 2 quy tắc khi viết Gherkin:
- Một file Gherkin chỉ mô tả cho một feature.
- Phần mở rộng của Gherkin là *.feature
Cucumber hỗ trợ viết hành vi kiểm thử cho khoảng 60 ngôn ngữ khác nhau
Tìm hiểu về cú pháp Gherkin
Cú pháp gherkin chia thành 3 thành phần chính là Feature, Scenario và step
Mỗi file gồm một Feature
- Mỗi Feature gồm nhiều Scenario, bắt đầu bằng từ khóa “Feature:”. Mỗi Feature là 1 chức năng
- Mỗi Scenario gồm nhiều step, bắt đầu bằng từ khóa “Scenario:”. Mỗi Scenario là một testcase.
- Mỗi step sẽ bắt đầu bằng các keyword như Given, When, Then, But hoặc And
Trong đó: “Given”: Mô tả ngữ cảnh ban đầu của hệ thống. Mục đích của Given là đưa hệ thống vào một trạng thái đã biết trước khi sử dụng (hoặc hệ thống bên ngoài) bắt đầu tương tác với hệ thống (trong bước When).
Nếu bạn đã làm việc với use case, Givens là điều kiện tiên quyết.“When”: Mô tả hành vi. Mục đích của When là để mô tả các sự kiện, hành động chính mà người dùng sử dụng.
“Then”: Mô tả kết quả. Mục đích của Then là quan sát kết quả. Các quan sát phải được liên quan đến các giá trị kinh doanh / lợi ích trong việc mô tả feature. Các quan sát phải kiểm tra đầu ra của hệ thống (một báo cáo, giao diện người dùng, tin nhắn,...)
“And”, “But”: Kết hợp nhiều step giống nhau
Cucumber không phân biệt các step. Tuy nhiên nên viết theo đúng mặt ngữ nghĩa để dễ dàng đọc hiểu.
Ví dụ về một file feature hoàn chỉnh
Feature: Home page
Scenario: Viewing application's home page
Given there's a post titled "My first" with "Hello, BDD world!" content
When I am on the homepage
Then I should see the "My first" post
Chu trình chương trình chạy test với Cucumber:
- Mô tả lại hành vi dưới dạng plain-text (sử dụng ngôn ngữ Gherhin)
- Định nghĩa các step
- Chạy test và xem test fail
- Viết code làm cho các step pass
- Chạy lại test và xem những step pass
- Lặp lại các bước đến khi toàn bộ các step pass
4.Javascript Testing với Jasmine
Javascript là một ngôn ngữ lập trình kịch bản. Từ khi bắt đầu khiêm tốn với việc xử lý các forms HTML, cùng với sự phát triển của nền tảng Web, Javascript đã đi rất xa. Hiện nay, Javascript là một trong những ngôn ngôn ngữ lập trình phổ biến nhất thế giới, các thư viện và framework Javascript xuất hiện rất nhiều, từ xử lý phía client như : Jquery, Angular.JS, Knockout.JS,... cho tới xử lý phía server như Node.JS , thậm chí các ứng dụng di động được tạo hoàn toàn với HTML, Javascript và CSS.
Jasmine là gì ?
Jasmine là một framework BDD (behavior-driven development) dành cho kiểm thử mã JavaScript. Framework này không phụ thuộc vào các framework JavaScript khác, không yêu cầu DOM. Và cú pháp rõ ràng, dễ dàng cho việc viết các kiểm thử.
Ví dụ: một ứng dụng chơi nhạc có một kiểm thử chấp nhận như sau:
Cho một người chơi nhạc nghe nhạc, khi bài hát tạm dừng ,thì phải chỉ ra bài hát hiện thời đang tạm dừng.
Theo BDD :
given: a player
when: the song has been paused
then: it should indicate that the song is currently paused.
Trong Jasmine, điều này được chuyển thành một ngôn ngữ rất biểu cảm cho phép các test case được viết theo cách tham chiếu đến các nghiệp vụ thực tế. Tiêu chí kiểm thử chấp nhận trên được viết Unit Test trong Jasmine như sau:
describe("Player", function() {
describe("when song has been paused", function() {
it("should indicate that the song is paused", function() {
});
});
});
Cài đặt Jasmine
Đường dẫn Opensource Jasmine:
https://github.com/jasmine/jasmine/releases
Sau khi download về và giải nén ta có thư mục:
Trong thư mục lib : Chứa các file thư viện của Jasmine
Thư mục spec: thư mục chứa các spec file ( file unit test)
Thư mục src : chứa file nguồn cần test.
File SpecRunner.htm là file chạy chương trình Jasmine JavaScript Testing
Chạy file SpecRunner.htm ta sẽ được kết quả như hình dưới:
View source file SpecRunner.htm ta được kết quả như dưới
Xây dựng Unit Test trong Jasmine
Để bắt đầu, chúng ta cần một ví dụ cụ thể: Giả sử bạn đang phát triển một phần mềm để theo dõi các khoản đầu tư vào thị trường chứng khoán.
Hình dưới minh họa cách người dùng có thể tạo một khoản đầu tư mới cho ứng dụng này:
Symbol : Nhập vào mã chứng khoán
Shares: Nhập vào số cổ phần mà người dùng đã mua ( hay khối lượng cổ phiếu đã mua)
Share Price: Giá của một cổ phiếu
Ý tưởng là hiển thị tình trạng của mã cổ phiếu đang xảy ra. Do giá cổ phiếu dao động theo thời gian, sự chênh lệch của giá hiện hành so với giá tại thời điểm mua cho thấy khoản đầu tư của người dùng là tốt (có lợi nhuận) hay xấu( lỗ).
Hình dưới đây hiển thị mã cổ phiếu AOUE đang lãi 101.80%, cổ phiếu PETO đang lỗ 42.34%
Jasmine cơ bản và tư duy trong BDD
Dựa trên ứng dụng được trình bày ở trên, chúng ta có thể viết các tiêu chí kiểm thử chấp nhận như sau:
- Để có một khoản đầu tư, cần phải có chứng khoán
Để có một khoản đầu tư, cần có số lượng cổ phần( mỗi cổ phần tương ứng với một cổ phiếu)
Để có một khoản đầu tư, cần có giá của một cổ phần ( hay giá một cổ phiếu)
Để có một khoản đầu tư, chắc chắn cần có một khoản chi phí
Sử dụng Jasmine Standalone đã tải về, việc đầu tiên chúng ta phải làm là tạo một file spec mới. File này có thể để bất cứ đâu, nhưng tốt nhất chúng ta nên theo một qui ước của Jasmine: file spec để trong thư mục /spec.
Tạo file _InvestmentSpec.js _và thêm vào dòng code sau:
describe("Investment", function() {
});
Hàm _describle _là một hàm toàn cục trong Jasmine được sử dụng để định nghĩa các ngữ cảnh kiểm tra.
Hàm describle chấp nhận hai tham số:
Tên của bộ kiểm tra- Trong trường hợp này : "Investment"
Một hàm bao gồm tất cả các test case có thể xảy ra trong bộ kiểm tra
Để chạy file kiểm tra, thêm đoạn code sau vào file chạy SpecRunner.htm :
<!-- include spec files here... -->
<script type="text/javascript" src="spec/InvestmentSpec.js"></script>
Chạy file SpecRunner.htm trên trình duyệt. Màn hình output như sau:
Để thêm các Unit Test, Jasmine cung cấp hàm it . Mỗi hàm it tương đương với một Unit Test.
Ví dụ: trong trường hợp "Để có một khoản đầu tư, cần phải có chứng khoán" ta có đoạn code như sau trong file InvestmentSpec.js
describe("Investment", function() {
it("should be of a stock", function() {
expect(investment.stock).toBe(stock);
});
});
Hàm expect chính là hàm kỳ vọng để kiểm tra tính đúng đắn của unit test.
Một xác nhận hay kỳ vọng là sự so sánh giữa hai giá trị và phải dẫn tới một giá trị boolean. Một khẳng định được cho là thành công nếu kết quả so sánh là đúng. Trong Jasmine, một khẳng định được viết với hàm expect kết hợp với một matcher cho biết những gì so sánh phải được thực hiện với những giá trị trong nó. Các matcher trong Jasmine sẽ được giới thiệu ở phần sau.
Chạy file SpecRunner.htm với InvestmentSpec.js có đoạn code trên ta được màn hình output như sau:
Spec này fail bởi vì, như trong message lỗi chỉ ra : "investment is not defined ".
Ý tưởng đặt ra ở đây là chúng ta chỉ cần làm những gì mà lỗi cho thấy chúng ta phải làm. Do đó chúng ta cần tạo ra biến _invesment _với một thể hiện Investment trong file InvestmentSpec.js , như sau:
describe("Investment", function() {
it("should be of a stock", function() {
var investment = new Investment();
expect(investment.stock).toBe(stock);
});
});
Tiếp theo, chạy lại SpecRunner.htm :
Bạn có thể thấy, thông báo lỗi đã thay đổi. Như lỗi chỉ ra, đối tượng Investment chưa được định nghĩa. Nghĩa là nó đòi hỏi phải khai báo một đối tượng là Investment. Do đó, chúng ta tạo mới một file Investment.js trong folder src và thêm mới vào file SpecRunner.htm như sau:
<!-- include source files here... -->
<script type="text/javascript" src="src/Investment.js"></script>
Để định nghĩa một đối tượng Investment, chúng ta viết hàm khởi tạo trong file Investment.js trong folder src :
function Investment () {};
Điều này làm cho thông báo lỗi khi chạy lại SpecRunner.htm thay đổi. Bây giờ nó thông báo lỗi về việc thiếu biến stock.
Một lần nữa, chúng ta thêm mã nguồn mà nó yêu cầu vào file InvestmentSpec.js như sau:
describe("Investment", function() {
it("should be of a stock", function() {
var stock = new Stock();
var investment = new Investment();
expect(investment.stock).toBe(stock);
});
})
Thông báo lỗi lại tiếp tục thay đổi, lần này là về việc thiếu đối tượng Stock:
Tạo file mới Stock.js trong thư mục src, và thêm mới vào trong SpecRunner.htm. Do đối tượng Stock sẽ là một phụ thuộc vào đối tượng Investment, chúng ta nên để nó lên trên đoạn thêm Investment.js. Đoạn code như sau:
<!-- include source files here... -->
<script type="text/javascript" src="src/Stock.js"></script>
<script type="text/javascript" src="src/Investment.js"></script>
Viết hàm khởi tạo Stock trong file Stock.js :
function Stock () {};
Cuối cùng là lỗi về kỳ vọng, như thể hiện trong ảnh dưới đây:
Để sửa lỗi này, mở file Investment.js trong folder src và thêm tham chiếu đến tham số stock:
function Investment (stock) {
this.stock = stock;
};
Trong spec file, chuyển stock thành tham số cho hàm Investment.
describe("Investment", function() {
it("should be of a stock", function() {
var stock = new Stock();
var investment = new Investment(stock);
expect(investment.stock).toBe(stock);
});
});
Cuối cùng , chúng ta có một spec hoàn chỉnh đã pass:
Chúng ta đi tiếp đến test case thứ hai : Để có một khoản đầu tư, cần có số lượng cổ phần
Trong file InvestmentSpec.js , bạn có thể thêm mới một spec " should have the invested shares' quantity" như sau:
describe("Investment", function() {
it("should be of a stock", function() {
var stock = new Stock();
var investment = new Investment({
stock: stock,
shares: 100
});
expect(investment.stock).toBe(stock); });
it("should have the invested shares' quantity", function() {
var stock = new Stock();
var investment = new Investment({
stock: stock,
shares: 100
});
expect(investment.shares).toEqual(100);
});
});
Có thể thấy, chúng ta đã thay đổi đoạn mã nguồn gọi đến khởi tạo Investment. Đoạn khởi tạo mới gọi tới một đối tương có hai thuộc tính là stock và shares, shares là tham số mới thêm vào . Chúng ta có thể chạy file spec để xem kết quả:
Do đó trong hàm khởi tạo ở Investment.js, ta thay đổi đoạn code như sau:
function Investment (params) {
this.stock = params.stock;
this.shares = params.shares;
};
Màn hình sau khi thêm đoạn code trên:
Nhưng như các bạn thấy, đoạn mã nguồn sau được gọi hai lần trong hai spec:
var stock = new Stock();
var investment = new Investment({
stock: stock,
shares: 100
});
Để tránh sự trùng lặp này, Jasmine cung cấp hàm cục bộ beforeEach . Hàm này sẽ được thực thi trước mỗi spec. Do đó, với hai specs ở trên, nó sẽ được chạy hai lần, trước mỗi spec.
describe("Investment", function() {
var stock, investment;
beforeEach(function() {
stock = new Stock();
investment = new Investment({
stock: stock,
shares: 100
});
});
it("should be of a stock", function() {
expect(investment.stock).toBe(stock);
});
it("should have the invested shares quantity", function() {
expect(investment.shares).toEqual(100);
});
});
Dùng các hàm này, chúng ta không chỉ xóa bỏ các đoạn mã trùng lặp, mà còn làm cho các đoạn spec của chúng ta đơn giản hơn. Chúng trở nên dễ đọc và dễ sửa đổi hơn.
Tương tự hàm beforeEach , jamine cung cấp các hàm :
- afterEach: hàm này sẽ được thực thi nhiều lần sau mỗi test
- beforeAll: hàm chạy một lần duy nhất trước các test case trong 1 describe
- afterAll: hàm chạy 1 lần duy nhất sau khi chạy hết các test case trong 1 descibe
Cuối cùng, chúng ta đi nốt hai test case còn lại trong bài toán đặt ra:
Thêm đoạn mã nguồn sau vào trong file InvestmentSpec.js
describe("Investment", function() {
var stock;
var investment;
beforeEach(function() {
stock = new Stock();
investment = new Investment({
stock: stock,
shares: 100,
sharePrice: 20
});
});
it("should be of a stock", function() {
expect(investment.stock).toBe(stock);
});
it("should have the invested shares quantity", function() {
expect(investment.shares).toEqual(100);
});
it("should have the share paid price", function() {
expect(investment.sharePrice).toEqual(20);
});
it("should have a cost", function() {
expect(investment.cost).toEqual(2000);
});
});
Chạy chương trình test, lỗi hiển thị như màn hình sau:
Thêm mới đoạn mã nguồn sau vào file Investment.js để vá lỗi:
function Investment (params) {
this.stock = params.stock;
this.shares = params.shares;
this.sharePrice = params.sharePrice;
this.cost = this.shares * this.sharePrice;
};
Chạy lại file SpecRunner.htm:
Matchers trong Jasmine
Trong các ví dụ trên, chúng ta đã nhìn thấy cách sử dụng toBe và toEqual matchers. Đây là hai matcher đã được xây dựng sẵn trong thư viện của Jasmine , nhưng chúng ta có thể xây dựng matchers riêng của chính chúng ta.
Customer matcher
Xây dựng matcher riêng khá đơn giản, bạn thực hiện việc này bằng cách gọi đến hàm jasmine.addMatcher, tốt nhất là đặt bên trong hàm beforeEarch
Ví dụ:
beforeEach(function() {
jasmine.addMatchers({
myMatcher: function() {}
});
});
Hàm ở đây được định nghĩa ở đây, bản thân nó không phải là một matcher. Mục đích của nó là khi được gọi, nó trả về một đối tượng có chức năng so sánh như sau:
jasmine.addMatchers({
myMatcher: function () {
return {
compare: function (actual, expected) {
// matcher definition
}
};
}
});
Hàm compare nhận hai giá trị được so sánh: giá trị thực tế "actual" và dự kiến "expect".
Matchers được xây dựng sẵn trong Jasmine:
toBe()
toEqual()
toBeDefined / toBeUndefined:
describe("Investment", function() {
it("investment is defined", function() {
var investment = new Investment();
expect(investment).toBeDefined();
});
it("investment is not defined", function() {
var investment;
expect(investment).toBeUndefined();
});
});
- toBeLessThan / toBeGreaterThan
it("is less than 10", function () {
expect(5).toBeLessThan(10);
});
it("is greater than 10", function () {
expect(20).toBeGreaterThan(10);
});
5. Tổng kết
- Khóa học cung cấp kiến thức cơ bản về Unit test
- Jasmine là một công cụ mã nguồn mở rất tiện dụng & dễ dùng để xây dựng kịch bản Unit test với Mã nguồn JavaScript
- Jasmine dựa trên nền BDD (behavior driven development) famework, xây dựng kịch bảng Unit test theo mô phỏng hướng hành vi nên trực quan trong việc xây dựng kịch bản test
- Jasmine cung cấp môi trường độc lập để kiểm thử và gọi chạy JavaScript
6. Tài liệu tham khảo
Jasmine Javascript Testing Second Editon - Paulo Ragonha
https://www.udacity.com/course/javascript-testing--ud549
https://toidicodedao.com/2015/04/02/viet-unit-test-cho-javascript-voi-jasmine/