Hẳn nhiều bạn vẫn còn bối rối khi nhắc đến việc test các component React đúng "chuẩn". Có cần phải tự viết tất cả các test hay không? Có nên test các props? Hay state? Styles/Layout?

Thực lòng mà nói chẳng có cách nào gọi là "chuẩn" ở đây cả, ít nhất là với quan điểm cá nhân của tôi. Tin mừng đó là tôi đã tìm được một số pattern và mẹo rất được việc, là những thứ mà tôi sẽ chia sẻ với các bạn ngay bây giờ.

 

Background: test ứng dụng

Giả sử rằng bạn muốn test component LockScreen. Component này hoạt động như là màn hình khóa trên điện thoại. Các chức năng của nó:

  • Hiển thị thời gian.
  • Hiển thị các tin nhắn đến.
  • Hiển thị ảnh nền.
  • Hiển thị một widget trượt-để-mở-khóa (slide-to-unlock) ở phía dưới màn hình.

Trực quan hơn thì là thế này:

Bạn có thể xem thêm ở đây, code ở Github.

Đây là code của component App:

import React from "react";
import LockScreen from "./LockScreen";

export default class App extends React.Component {
  render() {
    return (
      <LockScreen
        wallpaperPath="react_wallpaper.png"
        userInfoMessage="This is Tim's phone. If found, please give it back to him. He will be sad without it"
        onUnlocked={() => alert("unlocked!")}
      />
    );
  }
}

LockScreen  sẽ nhận vào 3 prop: wallpaperPath, userInfoMessage  và  onUnlocked.

Còn đây là code của component LockScreen:

import React, { PropTypes } from "react";
import ClockDisplay from "./ClockDisplay";
import TopOverlay from "./TopOverlay";
import SlideToUnlock from "./SlideToUnlock";

export default class LockScreen extends React.Component {
  static propTypes = {
    wallpaperPath: PropTypes.string,
    userInfoMessage: PropTypes.string,
    onUnlocked: PropTypes.func,
  };

  render() {
    const {
      wallpaperPath,
      userInfoMessage,
      onUnlocked,
    } = this.props;

    return (
      <div
        style={{
          height: "100%",
          display: "flex",
          justifyContent: "space-between",
          flexDirection: "column",
          backgroundImage: wallpaperPath ? `url(${wallpaperPath})` : "",
          backgroundColor: "black",
          backgroundPosition: "center",
          backgroundSize: "cover",
        }}
      >
        <ClockDisplay />
        {userInfoMessage ? (
          <TopOverlay
            style={{
              padding: "2em",
              marginBottom: "auto",
            }}
          >
            {userInfoMessage}
          </TopOverlay>
        ) : null}
        <SlideToUnlock onSlide={onUnlocked} />
      </div>
    );
  }
}

Mục tiêu của chúng ta sẽ là test component LockScreen.

 

Component Contracts

Để test LockScreen, bạn phải hiểu Contracts là gì. Hiểu được Contracts của component là phần quan trọng nhất trong việc test các React component. Một contract xác định các hành vi được dự kiến của component. Nếu không có contract đủ rõ ràng, các component sẽ trở nên rắc rối trong mắt người dùng. Viết test là một cách tuyệt vời để xác định các contract của component.

Mọi React component có ít nhất một thứ giúp định nghĩa contract của nó:

  • Nó sẽ render cái gì (hoặc không render cái gì).

Bên cạnh đó, phần lớn các contract của component bị ảnh hưởng bởi:

  • Các prop mà component nhận vào.
  • State của component.
  • Những hành vi component sẽ thực hiện khi người dùng tương tác (click, kéo thả, nhập vào từ bàn phím,...).

Một số điều ít ảnh hưởng hơn tới các contract như:

  • Nội dung mà component render.
  • Những hành vi component sẽ thực hiện khi bạn gọi đến các method của component.
  • Những hiệu ứng kèm theo trong chu trình của component ( componentDidMountcomponentWillUnmount,...).

Để xác định được contract của component, bạn cần trả lời được những câu hỏi sau:

  • Component của bạn sẽ render cái gì?
  • Liệu trong các tình huống khác nhau thì component của bạn có render ra các nội dung khác nhau hay không?
  • Khi truyền một function vào như một prop, component sẽ sử dụng function đó để làm gì? Nó sẽ gọi đến function đó, hay tiếp tục truyền function cho một component khác?
  • Khi người dùng tương tác với component, chuyện gì sẽ xảy ra?

 

Xác định contract của Lockscreen component

Cùng xem qua phương thức render của LockScreen, sau đó thêm các comment vào những vị trí mà dự đoán hành vi của nó trở nên không xác định. Hãy để ý các toán tử 3 ngôi, các câu lệnh if, switch,... Điều này sẽ giúp chúng ta nhận ra sự thay đổi của contract.

render() {
  const {
    wallpaperPath,
    userInfoMessage,
    onUnlocked,
  } = this.props;

  return (
    <div
      style={{
        height: "100%",
        display: "flex",
        justifyContent: "space-between",
        flexDirection: "column",
        // If a wallpaperPath prop was passed, then this div's CSS background-image
        // should be a url to that wallpaperPath. Otherwise, it should be an empty
        // string (which means the style should not be set).
        backgroundImage: wallpaperPath ? `url(${wallpaperPath})` : "",
        backgroundColor: "black",
        backgroundPosition: "center",
        backgroundSize: "cover",
      }}
    >
      <ClockDisplay />
      {/*
        If a userInfoMessage prop was passed, render that
        userInfoMessage within a TopOverlay. Otherwise,
        don't render anything here (null).
      */}
      {userInfoMessage ? (
        <TopOverlay
          style={{
            padding: "2em",
            marginBottom: "auto",
          }}
        >
          {userInfoMessage}
        </TopOverlay>
      ) : null}
      <SlideToUnlock onSlide={onUnlocked} />
    </div>
  );
}

Nhận xét:

  • 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 . 
  • Nếu 1 userInfoMessage prop được truyền vào, nó sẽ được truyền vào như là con của TopOverlay- là component được render cùng với các thuộc tính CSS inline-style cụ thể.
  • Nếu không truyền bất cứ userInfoMessage prop nào, TopOverlay component sẽ không được render.

Một số nhận xét sau đây thì luôn luôn đúng:

  • Thẻ div luôn luôn được render. Nó có một bộ các thuộc tính CSS inline-style cụ thể.
  • Component ClockDisplay luôn luôn được render. Tuy nhiên nó sẽ không nhận bất cứ prop nào.
  • Component SlideToUnlock luôn luôn được render. Nó lấy giá trị của prop onUnlocked và truyền vào prop onSlide của nó, bất kể việc onUnlocked đã được xác định hay chưa.

propTypes của component là nơi cần lưu ý trong việc xác định contract của component. Dựa vào đoạn code trên tôi có một số chú ý như sau:

  • wallpaperPath được mong là một chuỗi (tùy ý).
  • userInfoMessage được mong là một chuỗi (tùy ý luôn).
  • onUnlocked được mong là một hàm (tùy ý nốt).

Đây là một khỏi đầu tốt cho các contract của component. Đương nhiên sẽ vẫn còn nhiều ràng buộc với contract của các component, do đó bạn cần làm sáng tỏ chúng càng nhiều càng tốt trong code sản phẩm của bạn. Tuy nhiên trong bài viết này thì mục đích của chúng ta chính là làm việc với các contract. Các test luôn luôn có thể được thêm vào sau nếu có ràng buộc trong code.

 

Các test đáng giá như thế nào?

Hãy xem lại các contract ta vừa tìm được:

  • wallpaperPath được mong là một chuỗi (tùy ý).
  • userInfoMessage được mong là một chuỗi (tùy ý).
  • onUnlocked được mong là một hàm (tùy ý).
  • Thẻ div luôn luôn được render. Nó có một bộ các thuộc tính CSS inline-style cụ thể.
  • Component ClockDisplay luôn luôn được render. Tuy nhiên nó sẽ không nhận bất cứ prop nào.
  • Component SlideToUnlock luôn luôn được render. Nó lấy giá trị của prop onUnlocked và truyền vào prop onSlide của nó, bất kể việc onUnlocked đã được xác định hay chưa.
  • 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 . 
  • Nếu 1 userInfoMessage prop được truyền vào, nó sẽ được truyền vào như là con của TopOverlay- là component được render cùng với các thuộc tính CSS inline-style cụ thể.
  • Nếu không truyền bất cứ userInfoMessage prop nào, TopOverlay component sẽ không được render.

Một số ràng buộc cần phải test, một số khác thì không. Sau đây là một số luật mà tôi sử dụng để xác định cái nào không đáng để test:

  1. Test có lặp lại code ứng dụng hay không?
  2. Các assertion trong test có sinh ra các hành vi đã có trong các library hay không?
  3. Trên khía cạnh khách quan (đánh giá người dùng), chi tiết này có quan trọng hay không, hay chỉ là vấn đề bên trong ứng dụng? Liệu rằng các ảnh hưởng của chi tiết nội có được miêu tả chỉ bằng cách sử dụng các public API của component?

Cẩn thận đừng sử dụng những luật trên để bào chữa rằng case khó quá nên không test. Vì thông thường, các case khó thường mới là các case cần test. Bây giờ thì hãy dựa vào các luật trên để xác định case nào cần test. Đây là 3 cái đầu tiên:

  • wallpaperPath được mong là một chuỗi (tùy ý).
  • userInfoMessage được mong là một chuỗi (tùy ý).
  • onUnlocked được mong là một hàm (tùy ý).

Ba ràng buộc này là mối quan tâm của kỹ thuật PropTypes React, do đó viết test xung quang các prop types sẽ không đúng với luật #2 (đã có trong các library). Bởi vậy, tôi không test prop types. Bởi vì test thường được dùng làm documentation, tôi quyết định sẽ test case nào không đúng với luật #2 nếu: code ứng dụng không document tốt cho các type, tuy nhiên propTypes đã khá rõ ràng.

Đây là case kế:

  • Thẻ div luôn luôn được render. Nó có một bộ các thuộc tính CSS inline-style cụ thể.

Nó có thể tách ra thành 3 ràng buộc nhỏ:

  • Thẻ div luôn luôn được render.
  • Thẻ div đấy sẽ bao gồm tất cả mọi thứ được render.
  • Thẻ div đấy sẽ có một bộ các thuộc tính CSS inline-style cụ thể.

Hai ràng buộc đầu tiên không phạm bất kỳ luật nào, vậy nên ta sẽ test nó. Tuy nhiên hãy nhìn qua case thứ 3.

Bỏ qua phần thuộc tính background-image, thẻ div có style:

height: "100%",
display: "flex",
justifyContent: "space-between",
flexDirection: "column",
backgroundColor: "black",
backgroundPosition: "center",
backgroundSize: "cover",

Nếu ta viết test các style này, ta sẽ phải viết cho giá trị của từng style, chính xác theo thứ tự để làm cho assertion đúng. Do đó assertion sẽ trở thành như thế này:

  • Thẻ div cần có thuộc tính style là  Height   với giá trị 100%.
  • Thẻ div cần có thuộc tính display là flex .
  • Tương tự với từng thuộc tính css.

Cho dù chúng ta dùng các function như toMatchObject để giữ cho test ngắn gọn, điều này sẽ làm cho style bị lặp lại trong code của ứng dụng. Nếu ta thêm style khác vào, ta cần thêm chính xác đoạn code vào test. Do đó, ràng buộc này sẽ không đúng theo luật #1 (lặp code). Vì lý do này, tôi không test các inline style, trừ phi chúng thay đổi lúc thực thi.

Thông thường, nếu bạn viết test theo kiểu "nó làm những việc nó phải làm", "nó sẽ làm chính xác những công việc này, y như code ứng dụng", thì các test này thực sự chả cần thiết chút nào, hoặc phạm vi của nó sẽ quá rộng.

Hai ràng buộc tiếp theo:

  • Component ClockDisplay luôn luôn được render. Tuy nhiên nó sẽ không nhận bất cứ prop nào.
  • Component SlideToUnlock luôn luôn được render. Nó lấy giá trị của prop onUnlocked và truyền vào prop onSlide của nó, bất kể việc onUnlocked đã được xác định hay chưa.

Chia nhỏ vấn đề:

  • Component ClockDisplay luôn luôn được render.
  • Component ClockDisplay sẽ không nhận bất cứ prop nào.
  • Component SlideToUnlock luôn luôn được render.
  • 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.

Các ràng buộc này ứng với 2 hạng mục: "Một số component hỗn hợp được render" và "các component đã được render sẽ nhận vào các prop". Cả 2 đều cần phải test, do nó miêu tả cách component của bạn tương tác với các component khác.

Ràng buộc kế tiếp là:

  • 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.

Có thể bạn sẽ nghĩ do là inline-style nên chả cần phải test. Tuy nhiên giá trị của background-image có thể thay đổi do phụ thuộc vào prop wallpaperPath, vì lẽ đó nó cần phải được test. Nếu không test nó, sẽ dẫn đến không có test bao quanh wallpaperPath prop ( là thành phần public interface của component này ). Phải luôn luôn test các public interface.

Hai ràng buộc cuối cùng:

  • Nếu 1 userInfoMessage prop được truyền vào, nó sẽ được truyền vào như là con của TopOverlay- là component được render cùng với các thuộc tính CSS inline-style cụ thể.
  • Nếu không truyền bất cứ userInfoMessage prop nào, TopOverlay component sẽ không được render.

Sau khi chia nhỏ vấn đề:

  • 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 ).
  • Nếu 1 userInfoMessage prop được truyền vào, component  TopOverlay sẽ được render với một bộ các thuộc tính CSS inline-style cụ thể.
  • Nếu 1 userInfoMessage prop không được truyền vào, component TopOverlay sẽ không được render.

Ràng buộc thứ nhất và thứ 4 miêu tả ( TopOverlay sẽ được render/ TopOverlay sẽ không được render) cái mà chúng ta render, do đó ta cần test chúng.

Ràng buộc thứ 2 xác minh rằng component TopOverlay sẽ nhận vào một prop cụ thể dựa trên giá trị của userInfoMessage. Điều này là rất quan trọng khi viết test với các prop sẽ được truyền vào các component-đã-render, và đương nhiên ta cần test nó.

Ràng buộc thứ 3 xác minh rằng component TopOverlay sẽ nhận vào một prop cụ thể, do đó có thể bạn sẽ nhầm lẫn rằng ta sẽ test nó. Thực tế thì prop này chỉ là 1 loại inline-style nào đó. Chắc chắn rằng các prop được truyền vào thì quan trọng, tuy nhiên tạo các assertion cho các inline-style sẽ xung đột với luật #1 (lặp code). Test các prop được truyền vào rất quan trọng, do đó không thể chỉ dựa vào luật #1. Đó chính là lý do ta có luật #3:

Trên khía cạnh khách quan (đánh giá người dùng), chi tiết này có quan trọng hay không, hay chỉ là vấn đề bên trong ứng dụng? Liệu rằng các ảnh hưởng của chi tiết nội có được miêu tả chỉ bằng cách sử dụng các public API của component?

Khi viết các test cho component, tôi chỉ test các public API của component (bao gồm các ảnh hưởng kèm theo của API) khi nào khả dĩ. Tầng chính xác của component này sẽ không bị ảnh hưởng bởi các API của nó (đó là mỗi quan tâm của CSS engine). Do đó ràng buộc này sẽ không thỏa mãn luật #3. Nó không thỏa mãn cả luật #1 và #3 nên ta sẽ không test ràng buộc này, cho dù nó xác định rằng TopOverlay nhận vào một prop.

Để chắc chắn một ràng buộc có nên được test hay không là một điều khó khắn. Cuối cùng thì nó vẫn tùy thuộc vào bạn - bạn quyết định phần nào quan trọng để test. Lúc này, các luật tôi đã đưa ra ở trên sẽ giúp bạn.

Đến bây giờ thì ta đã đi qua hết các ràng buộc, biết được cần test cái nào. Tổng kết lại:

  • Thẻ div luôn luôn được render. 
  • Thẻ div đấy sẽ bao gồm tất cả mọi thứ được render.
  • Component ClockDisplay luôn luôn được render.
  • Component ClockDisplay sẽ không nhận bất cứ prop nào.
  • Component SlideToUnlock luôn luôn được render.
  • 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.
  • 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.
  • 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 ).
  • Nếu 1 userInfoMessage prop không được truyền vào, component TopOverlay sẽ không được render.

Bằng cách khảo sát ràng buộc, ta đã chia nhỏ chúng ra thành các thành phần nhỏ hơn. Điều này rất tốt cho việc viết test.

(Hết phần 1)

 

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