Bạn có thể xem phần 1 ở đây.

Cài đặt một số test boilerplate

Hãy khởi động với việc test component này. Tôi sẽ viết test với Jest và enzyme. Jest làm việc rất tốt với React và nó cũng là test runner đi kèm với ứng dụng tạo bởi create-react-app. Enzyme là một thư viện test React hoàn thiện, làm việc trên cả Node lẫn trình duyệt.

Mặc dù tôi sử dụng Jest và enzyme trong test, bạn có thể sử dụng code dưới đây để set up cho bất kì cấu hình test nào.

import React from "react";
import { mount } from "enzyme";
import LockScreen from "./LockScreen";

describe("LockScreen", () => {
  let props;
  let mountedLockScreen;
  const lockScreen = () => {
    if (!mountedLockScreen) {
      mountedLockScreen = mount(
        <LockScreen {...props} />
      );
    }
    return mountedLockScreen;
  }

  beforeEach(() => {
    props = {
      wallpaperPath: undefined,
      userInfoMessage: undefined,
      onUnlocked: undefined,
    };
    mountedLockScreen = undefined;
  });
  
  // All tests will go here
});

Giải thích:

  • Tôi sử dụng let với props và mountedLockScreen, nhờ đó mà các biến này sẽ truy cập đến được bên trong hàm describe.
  • Tôi tạo một hàm lockScreen luôn xác định bên trong hàm describe, hàm này sẽ sử dụng biến mountedLockScreen để mount một LockScreen với props hiện tại rồi trả về LockScreen đã được mount. Hàm này trả về một enzyme ReactWrapperTa sẽ sử dụng nó cho mọi test.
  • Tôi tạo một hàm beforeEach sẽ làm nhiệm vụ reset giá trị của props và mountedLockScreen trước khi chạy test. Mặt khác, state từ một test có thể ảnh hưởng đến các test khác. Bằng cách set cho biến mountedLockScreen giá trị undefined, mỗi khi chạy test, nếu nó gọi hàm lockScreen, một LockScreen mới sẽ được mount với props hiện tại.

Nội dung code trên đây giúp ta tập hợp các prop lại trước khi mount các component, khiến cho các test trở nên clean hơn. Tôi sử dụng nó cho tất cả các test component, và mong rằng các bạn sẽ thấy nó hữu ích. Những lợi ích của nó sẽ trở nên rõ ràng hơn khi ta viết các test case.

Tham khảo các khóa học lập trình online, onlab, và thực tập lập trình tại TechMaster

Viết test thôi

Ở mục trước ta đã có một danh sách các ràng buộc. Bay giờ ta sẽ viết test cho từng ràng buộc:

  • Thẻ div luôn luôn được render. 
it("always renders a div", () => {
  const divs = lockScreen().find("div");
  expect(divs.length).toBeGreaterThan(0);
});
  •  Thẻ div đấy sẽ bao gồm tất cả mọi thứ được render.
describe("the rendered div", () => {
  it("contains everything else that gets rendered", () => {
    const divs = lockScreen().find("div");
    // When using .find, enzyme arranges the nodes in order such
    // that the outermost node is first in the list. So we can
    // use .first() to get the outermost div.
    const wrappingDiv = divs.first();

    // Enzyme omits the outermost node when using the .children()
    // method on lockScreen(). This is annoying, but we can use it
    // to verify that wrappingDiv contains everything else this
    // component renders.
    expect(wrappingDiv.children()).toEqual(lockScreen().children());
  });
});
  • Component ClockDisplay luôn luôn được render.
it("always renders a `ClockDisplay`", () => {
  expect(lockScreen().find(ClockDisplay).length).toBe(1);
});
  • Component ClockDisplay sẽ không nhận bất cứ prop nào.
describe("rendered `ClockDisplay`", () => {
  it("does not receive any props", () => {
    const clockDisplay = lockScreen().find(ClockDisplay);
    expect(Object.keys(clockDisplay.props()).length).toBe(0);
  });
});
  • Component SlideToUnlock luôn luôn được render.
it("always renders a `SlideToUnlock`", () => {
  expect(lockScreen().find(SlideToUnlock).length).toBe(1);
});

Đến đây, các ràng buộc luôn luôn đúng nên viết test khá đơn giản. Tuy nhiên, các ràng buộc còn lại (bắt đầu với "Nếu.." hay "Khi...") tùy thuộc vào điều kiện mà sẽ đúng hay sai. Ta sẽ ghép cặp describe với beforeEach để test chúng. Đây là lúc cần sử dụng các test boilerplate ta đã viết.

  • Khi prop onUnlocked được xác định, component SlideToUnlock truyền giá trị của nó vào prop onSlide của chính component SlideToUnlock.
  • Khi prop onUnlocked là undefined, prop onSlide của component SlideToUnlock cũng là undefined.
describe("when `onUnlocked` is defined", () => {
  beforeEach(() => {
    props.onUnlocked = jest.fn();
  });

  it("sets the rendered `SlideToUnlock`'s `onSlide` prop to the same value as `onUnlocked`'", () => {
    const slideToUnlock = lockScreen().find(SlideToUnlock);
    expect(slideToUnlock.props().onSlide).toBe(props.onUnlocked);
  });
});

describe("when `onUnlocked` is undefined", () => {
  beforeEach(() => {
    props.onUnlocked = undefined;
  });

  it("sets the rendered `SlideToUnlock`'s `onSlide` prop to undefined'", () => {
    const slideToUnlock = lockScreen().find(SlideToUnlock);
    expect(slideToUnlock.props().onSlide).not.toBeDefined();
  });
});

Khi cần miêu tả các hành vi xảy ra bên trong một điều kiện nào đó, ta sẽ describe điều kiện đó, sau đó sử dụng beforeEach bên trong describe để thiết lập điều kiện.

  • Nếu 1 wallpaperPath prop được truyền vào, thẻ div ngoài cùng mà conponent render sẽ có 1 thuộc tính CSS tên là background-image (theo kiểu inline-style), hoàn toàn không phụ thuộc vào giá trị của wallpaperPath prop.
describe("when `wallpaperPath` is passed", () => {
  beforeEach(() => {
    props.wallpaperPath = "some/image.png";
  });

  it("applies that wallpaper as a background-image on the wrapping div", () => {
    const wrappingDiv = lockScreen().find("div").first();
    expect(wrappingDiv.props().style.backgroundImage).toBe(`url(${props.wallpaperPath})`);
  });
});
  • Nếu 1 userInfoMessage prop được truyền vào, TopOverlay sẽ được render.
  • Nếu 1 userInfoMessage prop được truyền vào, giá trị của nó sẽ được truyền cho con của component TopOverlay ( đã được render ).
describe("when `userInfoMessage` is passed", () => {
  beforeEach(() => {
    props.userInfoMessage = "This is my favorite phone!";
  });

  it("renders a `TopOverlay`", () => {
    expect(lockScreen().find(TopOverlay).length).toBe(1);
  });

  it("passes `userInfoMessage` to the rendered `TopOverlay` as `children`", () => {
    const topOverlay = lockScreen().find(TopOverlay);
    expect(topOverlay.props().children).toBe(props.userInfoMessage);
  });
});
  • Nếu 1 userInfoMessage prop không được truyền vào, component TopOverlay sẽ không được render.
describe("when `userInfoMessage` is undefined", () => {
  beforeEach(() => {
    props.userInfoMessage = undefined;
  });

  it("does not render a `TopOverlay`", () => {
    expect(lockScreen().find(TopOverlay).length).toBe(0);
  });
});

 Vậy là đã viết test xong cho tất cả các ràng buộc! Bạn có thể xem file test tổng kết ở đây.

"Đây đâu phải là việc của tôi!"

Khi nhìn vào ảnh động ở đầu bài viết, hẳn bạn sẽ mong các test case sẽ như thế này:

  • Khi người dùng kéo button slide-to-unlock hết về bên phải, một hàm callback sẽ được gọi.
  • Khi người dùng chỉ kéo slide-to-unlock nửa chừng rồi thả ra, button sẽ trở về vị trí cũ.
  • Đồng hồ trên màn hình sẽ luôn hiển thị thời gian hiện tại.

Điều này hoàn toàn dễ hiểu từ khía cạnh một ứng dụng, chúng là những tính năng dễ nhận thấy nhất.

Tuy nhiên ta lại không viết test cho bất kì tính năng nào vừa kể trên. Tại sao? Vì chúng không phải là mối bận tâm của LockScreen.

Component React là các đơn vị có thể tái sử dụng, unit test sẽ rất phù hợp với chúng. Và khi bạn viết unit test, chỉ nên viết test cho các thành phần mà component quan tâm. Thế nên đừng quá làm dụng test hết tất cả mà chỉ cần test đủ các ràng buộc cần thiết cho ứng dụng.

Đây là một tài liệu tham khảo khá có ích, nó liệt kê các mối quan tâm của phần lớn các component React:

  • Tôi sẽ làm gì với các prop nhận vào?
  • Tôi sẽ render component nào? Cần truyền vào chúng những gì?
  • Có nên lưu gì vào state không? Nếu có, nó có bị mất hiệu lực khi có các prop mới truyền vào không? Khi nào cần update state?
  • Nếu một component con gọi đến callback được truyền vào nó, tôi cần làm gì?
  • Chuyện gì sẽ xảy ra khi tôi mount? Khi tôi unmount?

Các tính năng miêu tả phía trên là mối quan tâm của SlideToUnlock và ClockDisplay, do đó test của những tính năng này sẽ đi kèm với các component tương ứng, nên không đề cập ở đây.

Tổng kết

Tôi mong rằng phương pháp này sẽ giúp các bạn tự viết được những test cho các component React. Tổng hợp lại có các bước sau cần thực hiện:

  • Tìm các contract component trước tiên.
  • Phân tích xem ràng buộc nào cần test, cái nào không?
  • Không cần test proptype.
  • Thông thường cũng không cần test inline-style.
  • Cần test: component bạn render và prop truyền vào component đó.
  • Nếu không phải mối quan tâm của các component, không test.

Nếu bạn không đồng ý hay thấy bài viết này có ích, hãy liên lạc với tôi qua tài khoản twitter. Hãy cùng nhau học cách viết test cho component React!

Bài viết được dịch từhttps://medium.freecodecamp.com/the-right-way-to-test-react-components-548a4736ab22#.mqp1qn88m