+1

Part 4: Làm thế nào để test class với Jest

Trong phần cuối cùng của series All things about unit test with Jest, mình sẽ hướng dẫn các bạn viết test cho class và các bạn sẽ thấy sức mạnh của spy được bộc lộ rõ như nào trong Test Double


Intro

Chúng ta đã đi qua hầu hết các phần quan trọng, và đây sẽ là bài cuối cùng kết thúc series này
Chúng ta cùng jump vào nó nào 😆 Các bạn có thể tham khảo toàn bộ source code của phần này ở đây

Setup

Ở phần này, mình sẽ xây dựng một lớp Dog để có thể test nó.
Đứng tại thư mục gốc, ta gõ lệnh sau:

$ pwd
/Users/tienminh/Documents/testing_with_jest
$ touch Dog.js

Sau đó các bạn hãy mở file Dog.js ra và tiến hành viết mã:

class Dog {
  constructor(name, breed, age) {
    this.name = name;
    this.breed = breed;
    this.age = age;
    this.energyLevel = 10;
  }

  bark() {
    console.log(`${this.name} barks: Woof woof!`);
  }

  fetch() {
    if (this.energyLevel > 0) {
      console.log(`${this.name} is fetching.`);
      this.energyLevel -= 2;
    } else {
      console.log(`${this.name} is too tired to fetch.`);
    }
  }

  describe() {
    console.log(`${this.name} is a ${this.age}-year-old ${this.breed}.`);
  }
}

module.exports = Dog;

Mình sẽ giải thích đôi chút về các hàm này:

  • constructor: phương thức khởi tạo
  • bark: phương thức mô tả cách sủa của chó : )
  • fetch: phương thức này giả định khi chó làm một việc gì đó sẽ mất năng lượng
  • describe: phương thức mô tả về chú chó này, gồm bao nhiêu tuổi, tên và giống chó

How to test class with Jest

Đầu tiên chúng ta cần tạo ra file test cho class Dog, chúng ta cũng sẽ tạo nó ngay ở thư mục gốc.

$ touch Dog.spec.js

1. Test class với Jest

Để test cho class, chúng ta sẽ sử dụng assertion để kiểm tra static methodinstance method có được định nghĩa không.

  • Thực chất test class cũng khá giống với test module, chỉ khác là ta cần phải khởi tạo nó trước khi test.
  • Jest cung cấp cho chúng ta jest.spyOn() function để có thể dễ dàng test class (đây chính là lúc sức mạnh của spy trong Test double được thể hiện 😇.
// Dog.spec.js
const Dog = require("./Dog.js");

describe("Dog", () => {
    let dog = new Dog('Buddy', 'Golden Retriever', 3);
    const barkSpy = jest.spyOn(dog, "bark");
    const fetchSpy = jest.spyOn(dog, "fetch");
    const describeSpy = jest.spyOn(dog, "describe");

    describe(".bark", () => {
        it ("defines a function", () => {
            expect(typeof dog.bark).toBe("function");
        })
    })

    describe(".fetch", () => {
        it ("defines a function", () => {
            expect(typeof dog.fetch).toBe("function");
        })
    })

    describe(".describe", () => {
        it ("defines a function", () => {
            expect(typeof dog.describe).toBe("function");
        })
    })
})

Mình sẽ giải thích ở đây

  • Ở đây mình chỉ kiểm tra xem các instance method có được khởi tạo hay không, có kiểu như nào.

NOTE

  • Chúng ta đã có khởi đầu tốt, tuy nhiên cho tới giờ thì chúng ta chỉ mới kiểm thử lớp interface (nghĩa là không đi sâu vào việc thực thi của từng instance method.

Tiếp theo đây chúng ta sẽ tiến hành test các method trong class.

2. Test các hàm trong class

// Dog.spec.js
const Dog = require("./Dog.js");

describe("Dog", () => {
    let dog = new Dog('Buddy', 'Golden Retriever', 3);
    
    const barkSpy = jest.spyOn(dog, "bark");
    const fetchSpy = jest.spyOn(dog, "fetch");
    const describeSpy = jest.spyOn(dog, "describe");
    const consoleSpy = jest.spyOn(console, "log");
    
    afterEach(() => {
        consoleSpy.mockClear();
    })

    describe(".bark", () => {
        it ("return undefined and console log when call", () => {
            expect(dog.bark()).toBeUndefined();
            expect(consoleSpy).toHaveBeenCalledWith("Buddy barks: Woof woof!");
            expect(barkSpy).toHaveBeenCalled();
        })
    })

    describe(".fetch", () => {
        describe("energy", () => {
            it ("greater than 0", () => {
                expect(dog.fetch()).toBeUndefined();
                expect(dog.energyLevel).toBe(8);
                expect(consoleSpy).toHaveBeenCalledWith("Buddy is fetching.")
                expect(fetchSpy).toHaveBeenCalled();
            })

            it ("less than 0", () => {
                // Arrange
                dog.energyLevel = 0;

                // act + assertion
                expect(dog.fetch()).toBeUndefined();
                expect(consoleSpy).toHaveBeenCalledWith("Buddy is too tired to fetch.")
                expect(fetchSpy).toHaveBeenCalled();
            })
        })
    })

    describe(".describe", () => {
        it ("return undefined and console log when call", () => {
            expect(dog.describe()).toBeUndefined();
            expect(consoleSpy).toHaveBeenCalledWith("Buddy is a 3-year-old Golden Retriever.");
            expect(describeSpy).toHaveBeenCalled();
        })
    })
})

Vài điều cần chú ý:

  • Chúng ta đã sử dụng spy khá nhiều để có thể test class từ interface cho đến implement function của 1 class.
  • Trên thực tế khi test class, chúng ta sẽ test hàm thực thi gốc, có nghĩa là chúng ta sẽ không thay đổi bất kì logic nào, mà chúng ta chỉ theo dõi nó.
  • Sự khác nhau giữa test moduletest class:
    • Test module cũng cần test theo hành vi của nó, tức là hành vi của nó ta sẽ giữ nguyên, tuy nhiên ta sẽ mock các external dependency bằng cách giả định kết quả của nó nhằm cung cấp input hợp lí để đưa đến output cần.
    • Test class ta sẽ cần khởi tạo object của class đó nhằm thực hiện test cho các instance method. Ngoài ra chúng ta cần test cho static method thì cũng làm tương tự như instance method.

NOTE:

  • Sức mạnh của spy được thể hiện khá rõ ở trong việc test class.

Kết luận

  • Sau khi trải qua 4 phần, chúng ta đã trải qua hết đủ về mô hình AAA, cũng như stub, spy, mock, dummy objectfake object.
  • Chúng ta cũng đã thấy việc dùng mock, dummy object khá là ít, và có thể coi gần như là ít sử dụng.
  • Chỉ cần có stubspy, chúng ta đã có thể test hầu hết các unit test
  • Tuy nhiên hiện nay thì khi code unit test, chúng ta không còn phân rõ như vậy mà sẽ sử dụng mock khá nhiều, vì nó thay thế được cả stub lẫn spy, tuy vậy việc phân biệt được các thành phần này trong unit test của các bạn giúp chúng ta sẽ code unit test tốt hơn, focus được vào nhiệm vụ cụ thể và cũng giúp tối ưu tài nguyên khi test nữa 🙆🏻‍♂️
  • Có thể mọi người nghĩ là việc viết unit test thì dù tốn thêm chút tài nguyên cũng không sao, tuy vậy việc viết unit test hiệu quả, hợp lý cũng giúp cho ứng dụng của chúng ta có độ tin cậy cao hơn. Hơn nữa việc viết unit test ngon cũng là việc chúng ta có thể làm, vậy tại sao lại không 🫶😄

Cảm ơn mọi người đã đọc hết series của mình 🙇🏼‍♂️

Reference


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.