Unit testing Các thành phần React với Enzyme và Jest

https://viblo.asia/p/test-javascript-with-jest-phan-1-V3m5W0VgKO7

Trong hướng dẫn này, chúng tôi sẽ viết bài kiểm tra đơn vị cho một ứng dụng todo cơ bản sử dụng Jest và React.

Bắt đầu nào!

Jest

Jest là một khung kiểm tra JavaScript được thiết kế để đảm bảo tính đúng đắn của bất kỳ cơ sở mã JavaScript nào. Nó cho phép bạn viết các bài kiểm tra với một API dễ tiếp cận, quen thuộc và giàu tính năng, cung cấp cho bạn kết quả nhanh chóng.

Jest được ghi chép đầy đủ, yêu cầu cấu hình nhỏ và có thể được mở rộng để phù hợp với yêu cầu của bạn. Để biết thêm thông tin về Jest, hãy kiểm tra tài liệu chính thức của nó.https://jestjs.io/docs/en/getting-started

Enzyme

Enzyme là một tiện ích Kiểm tra JavaScript dành cho React giúp kiểm tra đầu ra của các Thành phần React của bạn dễ dàng hơn. Bạn cũng có thể thao tác, duyệt và theo một số cách mô phỏng thời gian chạy cho đầu ra. Để biết thêm thông tin, hãy kiểm tra tài liệu chính thức của Enzyme.

https://enzymejs.github.io/enzyme/

Thiết lập

Trong hướng dẫn này, chúng tôi sẽ sử dụng công cụ CLI tạo ứng dụng để thiết lập dự án của chúng tôi. Vì vậy, hãy chuyển đến một thư mục nơi bạn sẽ lưu trữ dự án này và nhập nội dung sau vào terminal

create-react-app note-redux-app
npm install -g create-react-app
npm install --save-dev enzyme enzyme-adapter-react-16 enzyme-to-json

Trong thư mục src, tạo tệp tempPolyfills.js với nội dung sau. Điều này là cần thiết để thử nghiệm trên các trình duyệt cũ hơn.

const raf = global.requestAnimationFrame = (cb) => {
  setTimeout(cb, 0);
};
export default raf;
import raf from './tempPolyfills'
import Enzyme  from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
Enzyme.configure({ adapter: new Adapter() });

Trong tệp app.js , hãy thêm đoạn mã sau:

import React from 'react';
class App extends React.Component {
  render() {
    return(
      <div
        className='ui text container'
        id='app'
      >
        <table className='ui selectable structured large table'>
          <thead>
            <tr>
              <th>Items</th>
            </tr>
          </thead>
          <tbody>
            items
          </tbody>
          <tfoot>
            <tr>
              <th>
                <form
                  className='ui form'
                >
                <div className='field'>
                  <input
                    className='prompt'
                    type='text'
                    placeholder='Add item...'
                  />
                </div>
                <button
                  className='ui button'
                  type='submit'
                >
                  Add item
                </button>
                </form>
              </th>
            </tr>
          </tfoot>
        </table>
      </div>
    )
  }
}
export default App;

Hãy làm cho ứng dụng todo của chúng tôi hoạt động bằng đoạn mã sau

Đầu tiên, ứng dụng việc cần làm của chúng tôi cần một trạng thái để lưu trữ các mục việc cần làm và một mục việc cần làm.

Đoạn mã sau sẽ được thêm vào app.js :

state = {
    items: [],
    item: '',
};
<input
    className='prompt'
    type='text'
    placeholder='Add item...'
    value={this.state.item}
    onChange={this.onItemChange}
/>
onItemChange = (e) => {
    this.setState({
      item: e.target.value,
    });
  };

Nếu trường đầu vào trống, nút gửi sẽ bị tắt. Đối với tính năng này, hãy thêm đoạn mã bên dưới ngay sau phương thức hiển thị:

const submitDisabled = !this.state.item;
<button
  className='ui button'
  type='submit'
  disabled={submitDisabled}
>

Một onsubmitsự kiện sẽ được thêm vào thẻ biểu mẫu như sau:

onSubmit={this.addItem}
addItem = (e) => {
    e.preventDefault();
    this.setState({
      items: this.state.items.concat(
        this.state.item
      ),
      item: '',
    });
  };

Để liệt kê tất cả các mục việc cần làm, chúng ta cần lặp lại từng mục việc cần làm trong mảng mục.

<tbody>
  {
    this.state.items.map((item, idx) => (
      <tr
        key={idx}
      >
        <td>{item}</td>
      </tr>
    ))
  }
</tbody>
import React from 'react';
class App extends React.Component {
  state = {
    items: [],
    item: '',
  };
  onItemChange = (e) => {
    this.setState({
      item: e.target.value,
    });
  };
  addItem = (e) => {
    e.preventDefault();
    this.setState({
      items: this.state.items.concat(
        this.state.item
      ),
      item: '',
    });
  };
  render() {
    const submitDisabled = !this.state.item;
    return(
      <div
        className='ui text container'
        id='app'
      >
        <table className='ui selectable structured large table'>
          <thead>
            <tr>
              <th>Items</th>
            </tr>
          </thead>
          <tbody>
            {
              this.state.items.map((item, idx) => (
                <tr
                  key={idx}
                >
                  <td>{item}</td>
                </tr>
              ))
            }
          </tbody>
          <tfoot>
            <tr>
              <th>
                <form
                  className='ui form'
                  onSubmit={this.addItem}
                >
                <div className='field'>
                  <input
                    className='prompt'
                    type='text'
                    placeholder='Add item...'
                    value={this.state.item}
                    onChange={this.onItemChange}
                  />
                </div>
                <button
                  className='ui button'
                  type='submit'
                  disabled={submitDisabled}
                >
                  Add item
                </button>
                </form>
              </th>
            </tr>
          </tfoot>
        </table>
      </div>
    )
  }
}
export default App;

create-react-app thiết lập một bài kiểm tra giả cho chúng tôi trong app.test.jstệp. Hãy thực hiện kiểm tra ban đầu cho dự án của chúng tôi bằng lệnh sau trong thư mục dự án của chúng tôi.

Mở App.test.js và xóa tệp. Ở đầu tệp đó, trước tiên chúng ta nhập thành phần React mà chúng ta muốn kiểm tra, nhập React từ react và shallow()từ Enzyme. Các shallow()chức năng sẽ được sử dụng để nông làm cho các thành phần trong quá trình thử.

Trong trường hợp thử nghiệm đầu tiên của chúng tôi, chúng tôi sẽ khẳng định rằng bảng của chúng tôi sẽ hiển thị với tiêu đề là 'các mục'. Để viết khẳng định này, chúng ta cần:

• Shallow hiển thị thành phần • Duyệt qua DOM ảo, chọn ra thphần tử đầu tiên • Khẳng định rằng thphần tử chứa giá trị văn bản là “ Items

import App from './App';
import React from 'react';
import { shallow } from 'enzyme';
describe('App', () => {
  it('should have the `th` "Items"', () => {
    const wrapper = shallow(
      <App />
    );
    expect(
      wrapper.contains(<th>Items</th>)
    ).toBe(true);
  });
});

contains()chấp nhận một Phần tử React, trong trường hợp này là JSX đại diện cho một phần tử HTML. Nó trả về một boolean, cho biết thành phần được kết xuất có chứa HTML đó hay không.

Với thông số Enzyme đầu tiên của chúng tôi được viết, hãy xác minh mọi thứ hoạt động. SaveApp.test.jsvà chạy lệnh kiểm tra từ bảng điều khiển bằng lệnh sau:

npm test

Thêm đoạn mã bên dưới sau itkhối trước đó

it('should have a `button` element', () => {
    const wrapper = shallow(
      <App />
    );
    expect(
      wrapper.containsMatchingElement(
        <button>Add item</button>
      )
    ).toBe(true);
  });

Chúng ta không cần phải đối sánh thuộc tính-for bằng containsMatchingElement()phương thức này.

Tiếp theo, chúng tôi sẽ khẳng định rằng trường đầu vào cũng có:

it('should have an `input` element', () => {
    const wrapper = shallow(
      <App />
    );
    expect(
      wrapper.containsMatchingElement(
        <input />
      )
    ).toBe(true);
  });
it('`button` should be disabled', () => {
    const wrapper = shallow(
      <App />
    );
    const button = wrapper.find('button').first();
    expect(
      button.props().disabled
    ).toBe(true);
  });

Sử dụng trước

Trong tất cả các khung kiểm tra JavaScript phổ biến, có một chức năng mà chúng tôi có thể sử dụng để hỗ trợ thiết lập thử nghiệm: beforeEach. beforeEach là một khối mã sẽ chạy trước mỗi khối nó . Chúng ta có thể sử dụng hàm này để hiển thị thành phần của chúng ta trước mỗi thông số kỹ thuật.

Tại thời điểm này, bộ thử nghiệm của chúng tôi có một số mã lặp lại. Trong các khẳng định trước đây của chúng tôi, chúng tôi đã hiển thị nông thành phần trong mỗi khối nó. Để tránh những lần lặp lại này, chúng tôi sẽ cấu trúc lại xác nhận của mình. Chúng tôi sẽ chỉ hiển thị nông thành phần ở đầu khối mô tả của chúng tôi:

Bộ đồ thử nghiệm được tái cấu trúc của chúng tôi sẽ trông giống như sau:

describe('App', () => {
  let wrapper;
  beforeEach(() => {
    wrapper = shallow(
      <App />
    );
  });
  it('should have the `th` "Items"', () => {
    expect(
      wrapper.contains(<th>Items</th>)
    ).toBe(true);
  });
  it('should have a `button` element', () => {
    expect(
      wrapper.containsMatchingElement(
        <button>Add item</button>
      )
    ).toBe(true);
  });
  it('should have an `input` element', () => {
    expect(
      wrapper.containsMatchingElement(
        <input />
      )
    ).toBe(true);
  });
  it('`button` should be disabled', () => {
    const button = wrapper.find('button').first();
    expect(
      button.props().disabled
    ).toBe(true);
  });
});

Tương tác đầu tiên mà người dùng có thể có với ứng dụng của chúng tôi là điền vào trường đầu vào để thêm một mục mới. Chúng tôi sẽ khai báo một khối mô tả khác bên trong khối hiện tại của chúng tôi để nhóm các bộ thử nghiệm cho các tương tác của người dùng. các khối mô tả là cách chúng tôi "nhóm" các thông số kỹ thuật mà tất cả đều yêu cầu cùng một ngữ cảnh.

BeforeEach mà chúng ta viết cho mô tả bên trong của chúng ta sẽ được chạy sau mỗi before được khai báo trong ngữ cảnh bên ngoài. Do đó, trình bao bọc sẽ được hiển thị nông vào thời điểm nó beforeEachchạy. Như mong đợi, điều này beforeEachsẽ chỉ được chạy cho các khối bên trong khối mô tả bên trong của chúng tôi.

Chúng tôi sẽ sử dụng phương pháp mô phỏng để mô phỏng các tương tác của người dùng.

Phương thức mô phỏng chấp nhận hai đối số:

  1. Sự kiện cần mô phỏng (chẳng hạn như 'thay đổi' hoặc 'nhấp chuột'). Điều này xác định trình xử lý sự kiện sẽ sử dụng (chẳng hạn như onChange hoặc onClick).

  2. Đối tượng sự kiện (tùy chọn)

Vì vậy, bây giờ chúng ta có thể viết các thông số kỹ thuật liên quan đến ngữ cảnh mà người dùng vừa điền trường đầu vào. Chúng tôi sẽ viết hai thông số kỹ thuật:

Mục thuộc tính trạng thái đã được cập nhật để khớp với trường đầu vào. Nút đó không còn bị tắt nữa.

describe('the user populates the input', () => {
    const item = 'Laundry';
    beforeEach(() => {
      const input = wrapper.find('input').first();
      input.simulate('change', {
        target: { value: item }
      })
    });
    it('should update the state property `item`', () => {
      expect(
        wrapper.state().item
      ).toEqual(item);
    });
    it('should enable `button`', () => {
      const button = wrapper.find('button').first();
      expect(
        button.props().disabled
      ).toBe(false);
    });
  });

Sau khi người dùng đã điền vào trường nhập, Có hai hành động mà người dùng có thể thực hiện từ đây mà chúng tôi có thể viết thông số kỹ thuật cho:

  1. Người dùng xóa trường nhập

  2. Người dùng nhấp vào nút "Thêm mặt hàng"

Khi người dùng xóa trường nhập, chúng tôi hy vọng nút sẽ bị vô hiệu hóa trở lại. Chúng tôi sẽ xây dựng dựa trên ngữ cảnh hiện có của chúng tôi cho mô tả “người dùng điền thông tin đầu vào” bằng cách lồng mô tả mới của chúng tôi vào bên trong:

describe('and then clears the input', () => {
  beforeEach(() => {
    const input = wrapper.find('input').first();
    input.simulate('change', {
      target: { value: '' }
    })
  });
  it('should disable `button`', () => {
    const button = wrapper.find('button').first();
    expect(
      button.props().disabled
    ).toBe(true);
  });
});

Tiếp theo, chúng tôi sẽ mô phỏng người dùng gửi biểu mẫu.

Mô phỏng gửi biểu mẫu

Sau khi người dùng đã gửi biểu mẫu, Chúng tôi sẽ khẳng định rằng:

1. Mặt hàng mới ở trạng thái (các mặt hàng)

2. Mục mới nằm trong bảng kết xuất

3. Trường đầu vào trống

4. Nút “Thêm mục” bị tắt

Vì vậy, chúng tôi sẽ viết khối mô tả của chúng tôi bên trong “người dùng điền đầu vào” dưới dạng anh chị em để “và sau đó xóa đầu vào”:

describe('and then submits the form', () => {
      beforeEach(() => {
        const form = wrapper.find('form').first();
        form.simulate('submit', {
          preventDefault: () => {},
        });
      });
      it('should add the item to state', () => {
});
      it('should render the item in the table', () => {
});
      it('should clear the input field', () => {
});
      it('should disable `button`', () => {
});
    });

Với hàm beforeEach () của chúng tôi, trước tiên, chúng tôi khẳng định rằng mục mới ở trạng thái:

it('should add the item to state', () => {
  expect(
    wrapper.state().items
  ).toContain(item);
});

Tiếp theo, hãy khẳng định rằng mục đó nằm bên trong bảng.

it('should render the item in the table', () => {
  expect(
    wrapper.containsMatchingElement(
      <td>{item}</td>
    )
  ).toBe(true);
});
it('should clear the input field', () => {
  const input = wrapper.find('input').first();
  expect(
    input.props().value
  ).toEqual('');
});
it('should disable `button`', () => {
  const button = wrapper.find('button').first();
  expect(
    button.props().disabled
  ).toBe(true);
});
import App from './App';
import React from 'react';
import { shallow } from 'enzyme';
describe('App', () => {
  let wrapper;
  beforeEach(() => {
    wrapper = shallow(
      <App />
    );
  });
  it('should have the `th` "Items"', () => {
    expect(
      wrapper.contains(<th>Items</th>)
    ).toBe(true);
  });
  it('should have a `button` element', () => {
    expect(
      wrapper.containsMatchingElement(
        <button>Add item</button>
      )
    ).toBe(true);
  });
  it('should have an `input` element', () => {
    expect(
      wrapper.containsMatchingElement(
        <input />
      )
    ).toBe(true);
  });
  it('`button` should be disabled', () => {
    const button = wrapper.find('button').first();
    expect(
      button.props().disabled
    ).toBe(true);
  });
  describe('the user populates the input', () => {
    const item = 'Vancouver';
    beforeEach(() => {
      const input = wrapper.find('input').first();
      input.simulate('change', {
        target: { value: item }
      });
    });
    it('should update the state property `item`', () => {
      expect(
        wrapper.state().item
      ).toEqual(item);
    });
    it('should enable `button`', () => {
      const button = wrapper.find('button').first();
      expect(
        button.props().disabled
      ).toBe(false);
    });
    describe('and then clears the input', () => {
      beforeEach(() => {
        const input = wrapper.find('input').first();
        input.simulate('change', {
          target: { value: '' }
        })
      });
      it('should disable `button`', () => {
        const button = wrapper.find('button').first();
        expect(
          button.props().disabled
        ).toBe(true);
      });
    });
    describe('and then submits the form', () => {
      beforeEach(() => {
        const form = wrapper.find('form').first();
        form.simulate('submit', {
          preventDefault: () => {},
        });
      });
      it('should add the item to state', () => {
        expect(
          wrapper.state().items
        ).toContain(item);
      });
      it('should render the item in the table', () => {
        expect(
          wrapper.containsMatchingElement(
            <td>{item}</td>
          )
        ).toBe(true);
      });
      it('should clear the input field', () => {
        const input = wrapper.find('input').first();
        expect(
          input.props().value
        ).toEqual('');
      });
      it('should disable `button`', () => {
        const button = wrapper.find('button').first();
        expect(
          button.props().disabled
        ).toBe(true);
      });
    });
  });
});

Tìm kiếm Bảng điều khiển React?

  • Hãy dùng thử Bảng điều khiển React của chúng tôi và tạo các ứng dụng web tuyệt đẹp cho các dự án khách hàng và dự án cá nhân không giới hạn.

  • Bắt đầu xây dựng các ứng dụng và sản phẩm web bằng các Mẫu React Miễn phí của chúng tôi mà không cần đầu tư.

Tổng cộng, cho đến nay chúng tôi đã học cách tổ chức mã thử nghiệm của mình theo cách định hướng hành vi, kết xuất nông với Enzyme. Cách sử dụng các phương thức Wrapper nông để duyệt qua DOM ảo, cách sử dụng các trình so khớp Jest để viết các loại xác nhận khác nhau (chẳng hạn như toContain()cho mảng). Cuối cùng, chúng tôi đã thấy cách chúng tôi có thể sử dụng phương pháp tiếp cận theo hướng hành vi để thúc đẩy thành phần của bộ thử nghiệm trong phản ứng bằng cách sử dụng khung thử nghiệm Jest và Enzyme.

Last updated