反応部品の試験—反応試験ライブラリによる正しい方法


数日前、反応コンポーネント/アプリケーションをテストするためのテストパッケージ(反応テストライブラリ)をリリースしました.パッケージは、良いテストプラクティスを奨励する原則に基づいて作成されました.

The more your tests resemble the way your software is used, the more confidence they can give you.


ライティングテストを書くことは複雑で挑戦的で、ワーカビリティーとユーザのインタラクションとインターフェイスの実装詳細をテストする一般的なドグマのために.このライブラリは、すべてのユーザーがどのように機能を実装されているだけでなく、それらと対話する方法に基づいてアプリケーションをテストすることです.
違いを理解するために、これら二つのテストスイートを見てください.
実装の詳細をテストする

test('todo should be set on state when input changes')
test('a list of todos should be set on state when component mounts')
test('the addTodo function should be called when user clicks createTodo button')

ソフトウェアが本当に機能するテストの考え方

test('clicking on the add todo button adds a new todo to the list')
test('gets todos when component is mounted and displays them as a list')
test('should show todo successfully created notification for two seconds when todo is created')

テストスイートで通知することができますように、このパッケージは、アプリケーションを展開するときに大いに信頼性を向上させる、より多くの統合テストを書くことを推奨します.
例えば、私たちはtodosのリストがどのようにレンダリングされるかに興味がありません、我々が興味を持っているものは、ユーザーがtodosのリストを見るようになるということです.また、入力テキストフィールドに変更がどのようにコンポーネント状態によって管理されるかについて心配したくないです、しかし、我々はユーザーが経験するものについて心配します、そして、それは我々がテストするつもりです.

背景:我々はテストするアプリ:


私たちは、いくつかのテストを書きますhere .
ここでは、アプリケーションによって提供される機能の一覧を示します.
  • コンポーネントがマウントされているときにAPIからtodosの一覧を表示する
  • 追加、編集、および更新をtodos.
  • 異なるアクションの通知を示します.
  • 以下のテストを書きます.
  • コンポーネントがマウントされているときにAPIからtodosの一覧を表示する
  • 追加する
  • アプリケーションはスキャフォールドでしたcreate-react-app .メインファイルは以下の通りです.App.js ファイル
    import PropTypes from 'prop-types';
    import React, { Component } from 'react';
    
    import './App.css';
    import logo from './logo.svg';
    import ListItem from './ListItem';
    import loadingGif from './loading.gif';
    
    
    class App extends Component {
      constructor() {
        super();
        this.state = {
          newTodo: '',
          editing: false,
          editingIndex: null,
          notification: null,
          todos: [],
          loading: true
        };
    
        this.addTodo = this.addTodo.bind(this);
        this.updateTodo = this.updateTodo.bind(this);
        this.deleteTodo = this.deleteTodo.bind(this);
        this.handleChange = this.handleChange.bind(this);
        this.hideNotification = this.hideNotification.bind(this);
      }
    
      async componentDidMount() {
        const todos = await this.props.todoService.getTodos();
        this.setState({
          todos,
          loading: false
        });
      }
    
      handleChange(event) {
        this.setState({
          newTodo: event.target.value
        });
      }
    
      async addTodo() {
        const todo = await this.props.todoService.addTodo(this.state.newTodo);
    
        this.setState({
          todos: [
            ...this.state.todos, todo
          ],
          newTodo: '',
          notification: 'Todo added successfully.'
        }, () => this.hideNotification());
      }
    
      editTodo(index) {
        const todo = this.state.todos[index];
        this.setState({
          editing: true,
          newTodo: todo.name,
          editingIndex: index
        });
      }
    
      async updateTodo() {
        const todo = this.state.todos[this.state.editingIndex];
        const updatedTodo = await this.props.todoService.updateTodo(todo.id, this.state.newTodo);
        const todos = [ ...this.state.todos ];
        todos[this.state.editingIndex] = updatedTodo;
        this.setState({
          todos,
          editing: false,
          editingIndex: null,
          newTodo: '',
          notification: 'Todo updated successfully.'
        }, () => this.hideNotification());
      }
    
      hideNotification(notification) {
        setTimeout(() => {
          this.setState({
            notification: null
          });
        }, 2000);
      }
    
      async deleteTodo(index) {
        const todo = this.state.todos[index];
    
        await this.props.todoService.deleteTodo(todo.id);
    
        this.setState({ 
          todos: [
            ...this.state.todos.slice(0, index),
            ...this.state.todos.slice(index + 1)
          ],
          notification: 'Todo deleted successfully.'
        }, () => this.hideNotification());
      }
    
      render() {
        return (
          <div className="App">
            <header className="App-header">
              <img src={logo} className="App-logo" alt="logo" />
              <h1 className="App-title">CRUD React</h1>
            </header>
            <div className="container">
              {
                this.state.notification &&
                <div className="alert mt-3 alert-success">
                  <p className="text-center">{this.state.notification}</p>
                </div>
              }
              <input
                type="text"
                name="todo"
                className="my-4 form-control"
                placeholder="Add a new todo"
                onChange={this.handleChange}
                value={this.state.newTodo}
              />
              <button
                onClick={this.state.editing ? this.updateTodo : this.addTodo}
                className="btn-success mb-3 form-control"
                disabled={this.state.newTodo.length < 5}
              >
                {this.state.editing ? 'Update todo' : 'Add todo'}
              </button>
              {
                this.state.loading &&
                <img src={loadingGif} alt=""/>
              }
              {
                (!this.state.editing || this.state.loading) &&
                <ul className="list-group">
                  {this.state.todos.map((item, index) => {
                    return <ListItem
                      key={item.id}
                      item={item}
                      editTodo={() => { this.editTodo(index); }}
                      deleteTodo={() => { this.deleteTodo(index); }}
                    />;
                  })}
                </ul>
              }
            </div>
          </div>
        );
      }
    }
    
    App.propTypes = {
      todoService: PropTypes.shape({
        getTodos: PropTypes.func.isRequired,
        addTodo: PropTypes.func.isRequired,
        updateTodo: PropTypes.func.isRequired,
        deleteTodo: PropTypes.func.isRequired
      })
    };
    
    export default App;
    
    
    ListItem.js ファイル
    
    import React from 'react';
    import PropTypes from 'prop-types';
    
    const ListItem = ({ editTodo, item, deleteTodo }) => {
      return <li
        className="list-group-item"
      >
        <button
          className="btn-sm mr-4 btn btn-info"
          onClick={editTodo}
        >U</button>
        {item.name}
        <button
          className="btn-sm ml-4 btn btn-danger"
          onClick={deleteTodo}
        >X</button>
      </li>;
    };
    
    ListItem.propTypes = {
      editTodo: PropTypes.func.isRequired,
      item: PropTypes.shape({
        id: PropTypes.number.isRequired,
        name: PropTypes.string.isRequired
      }),
      deleteTodo: PropTypes.func.isRequired
    };
    
    export default ListItem;
    
    
    
    index.js ファイル
    
    import React from 'react';
    import axios from 'axios';
    import ReactDOM from 'react-dom';
    
    import App from './App';
    import { apiUrl } from './config';
    
    import TodoService from './service/Todo';
    
    const client = axios.create({
      baseURL: apiUrl,
    });
    
    const todoService = new TodoService(client);
    
    ReactDOM.render(<App todoService={todoService} />, document.getElementById('root'));
    
    
    
    TodoService.js ファイル
    
    
    /**
     * A todo service that communicates with the api to perform CRUD on todos.
     */
    export default class TodoService {
      constructor(client) {
        this.client = client;
      }
    
      async getTodos() {
        const { data } = await this.client.get('/todos');
        return data;
      }
    
      async addTodo(name) {
        const { data } = await this.client.post('/todos', { name });
    
        return data;
      }
    
      async updateTodo(id, name) {
        const { data } = await this.client.put(`/todos/${id}`, { name });
    
        return data;
      }
    
      async deleteTodo (id) {
        await this.client.delete(`/todos/${id}`);
    
        return true;
      }
    }
    
    
    
    テストから始めるために必要なものをすべて設定しましょう.あなたが使っているならばcreate-react-app (として、テスト環境は既に設定しています.すべての左側に反応テストライブラリをインストールすることです.
    
    npm i --save-dev react-testing-library
    
    

    Test :コンポーネントがマウントされたときに、リストの一覧を表示します。


    アウトコンポーネントがマウントされたときに発生する最初のことのテストを書くことから始めましょう.todosはAPIから取得され、リストとして表示されます.App.spec.js ファイル
    
    import React from 'react'; 
    import { render, Simulate, flushPromises } from 'react-testing-library';
    
    
    import App from './App';
    import FakeTodoService from './service/FakeTodoService';
    
    
    describe('The App component', () => {
        test('gets todos when component is mounted and displays them', async () => {
    
        });
    });
    
    
    まず、輸入render を使用してシーンの背後にコンポーネントをマウントする単純なヘルパー機能ですReactDOM.render , そして、マウントされたDOMコンポーネントと私たちのテストのためのヘルパー関数のカップルに戻ります.
    第二に、我々は輸入Simulate , これは全く同じシミュレーションですreact-dom . それは私たちのテストのユーザーイベントをシミュレートするのに役立ちます.
    最後に、我々は輸入flushPromises , あなたのコンポーネントが何らかのasync仕事をしているとき、役に立ちます、そして、あなたがあなたの主張を続けることができる前に、非同期操作が解決するか(または拒絶する)ことを確認する必要がある単純なユーティリティです.
    この文書の時点で、それはパッケージのAPIに関するすべてです.きれいなきちんとした?
    また、私が輸入した通知FakeTodoService , これは私たちのテストの外部のasync機能をモッキングする私のバージョンです.あなたは本当の使用を好むかもしれないTodoService , そして、axios ライブラリは、すべてあなたに.以下に、偽のtodoサービスの様子を示します.
    
    
    /**
     * A fake todo service for mocking the real one.
     */
    export default class FakeTodoService {
      constructor(todos) {
        this.todos = todos ? todos : [];
      }
      async getTodos() {
        return this.todos;
      }
    
      async addTodo(name) {
        return {
          id: 4,
          name
        };
      }
    
      async deleteTodo(id) {
        return true;
      }
    
      async updateTodo(id, name) {
        return {
          id, name
        };
      }
    }
    
    
    
    
    我々のコンポーネントがマウントされるとすぐに、それはAPIからtodosをフェッチして、これらのtodosを表示することを確認したいです.我々がしなければならないすべては、我々の偽のtodoサービスでこの構成要素をマウントして、我々の偽のサービスからのtodosが正しく表示されると断言することです?見てみる
    
    describe('The App component', () => {
        const todos = [{
          id: 1,
          name: 'Make hair',
        }, {
          id: 2,
          name: 'Buy some KFC',
        }];
    
        const todoService = new FakeTodoService(todos);
    
        test('gets todos when component is mounted and displays them', async () => {
            const { container, getByTestId } = render(<App todoService={todoService} />);
    
        });
    });
    
    
    このコンポーネントをレンダリングすると、結果から2つのものを構造化しますcontainer , とgetByTestId . コンテナはマウントされたDOMコンポーネントであり、getByTestId DOMにデータ属性を使用して要素を検索する単純なヘルパー関数です.見るthis article by Kent C. Dodds クラスやIDのような伝統的なCSSセレクタではなくデータ属性を使うのが望ましい理由を理解する.コンポーネントをマウントした後に、todosが表示されることを確認するためにtestid データは、我々のtodo要素を含んでいて、子供に期待を書きます.
    
    // App.js
    
    
    ...
    
    
    {
       (!this.state.editing || this.state.loading) &&
           <ul className="list-group" data-testid="todos-ul">
    
    ...
    
    
    
    
    // App.test.js
    
    test('gets todos when component is mounted and displays them', async () => {
      const { container, getByTestId } = render(<App todoService={todoService} />);
      const unorderedListOfTodos = getByTestId('todos-ul');
      expect(unorderedListOfTodos.children.length).toBe(2);  
    });
    
    
    このテストをこの時点で実行すると失敗します.なぜですか.まあそれはどこflushPromises 関数が入る.我々は、後に我々の主張を実行する必要がありますgetTodos ToDoSサービスの機能はToDOSのリストで解決しました.その約束を解決するのを待つawait flushPromises() .
    
    // App.test.js
    
    test('gets todos when component is mounted and displays them', async () => {
       const { container, getByTestId } = render(<App todoService={todoService} />);
       await flushPromises();
       const unorderedListOfTodos = getByTestId('todos-ul');
       expect(unorderedListOfTodos.children.length).toBe(2);  
    });
    
    
    よろしい.これは、コンポーネントがマウントされるとすぐに確認することを気にします、私は、追加する良い主張がそれを確かめることであると思いますtodoService.getTodos 関数は、コンポーネントがマウントされたときに呼び出されます.これは、todosが実際に外部APIから来ているという事実に対する自信を増します.
    
    // App.test.js
    
    test('gets todos when component is mounted and displays them', async () => {
       // Spy on getTodos function 
       const getTodosSpy = jest.spyOn(todoService, 'getTodos');
    
       // Mount the component
       const { container, getByTestId } = render(<App todoService={todoService} />);
    
       // Wait for the promise that fetches todos to resolve so that the list of todos can be displayed
       await flushPromises();
    
       // Find the unordered list of todos
       const unorderedListOfTodos = getByTestId('todos-ul');
    
       // Expect that it has two children, since our service returns 2 todos.
       expect(unorderedListOfTodos.children.length).toBe(2);  
    
       // Expect that the spy was called
       expect(getTodosSpy).toHaveBeenCalled();
    });
    
    

    テスト: todosを追加する


    ToDo作成プロセスのテストを書きましょう.再び、ユーザーがアプリケーションと対話するときに起こることに興味があります.
    我々は、確かに確認することから始めますAdd Todo ユーザーが入力ボックスに十分な文字を入力していない場合は、ボタンが無効になります.
    
    // App.js
    // Add a data-testid attribute to the input element, and the button element
    
    ...
    
    <input
       type="text"
       name="todo"
       className="my-4 form-control"
       placeholder="Add a new todo"
       onChange={this.handleChange}
       value={this.state.newTodo}
       data-testid="todo-input"
    />
    
    <button
       onClick={this.state.editing ? this.updateTodo : this.addTodo}
       className="btn-success mb-3 form-control"
       disabled={this.state.newTodo.length < 5}
       data-testid="todo-button"
    >
     {this.state.editing ? 'Update todo' : 'Add todo'}
    </button>
    
    ...
    
    
    
    
    // App.test.js
    
    describe('creating todos', () => {
       test('the add todo button is disabled if user types in a todo with less than 5 characters', async () => {
         // Mount the component
         const { container, getByTestId } = render(<App todoService={todoService} />);
    
         // Wait for the promise that fetches todos to resolve so that the list of todos can be displayed
         await flushPromises();
    
        // Find the add-todo button and the todo-input element using their data-testid attributes
         const addTodoButton = getByTestId('todo-button');
         const todoInputElement = getByTestId('todo-input');
      });
    });
    
    
    追加data-testid への属性buttoninput 我々のテストではgetByTestId ヘルパー機能を見つけること.
    
    // App.test.js
    
    describe('creating todos', () => {
       test('the add todo button is disabled if user types in a todo with less than 5 characters, and enabled otherwise', async () => {
         // Mount the component
         const { container, getByTestId } = render(<App todoService={todoService} />);
    
         // Wait for the promise that fetches todos to resolve so that the list of todos can be displayed
         await flushPromises();
    
        // Find the add-todo button and the todo-input element using their data-testid attributes
         const addTodoButton = getByTestId('todo-button');
         const todoInputElement = getByTestId('todo-input');
    
        // Expect that at this point when the input value is empty, the button is disabled.
        expect(addTodoButton.disabled).toBe(true);
    
        // Change the value of the input to have four characters
        todoInputElement.value = 'ABCD';
        Simulate.change(todoInputElement);
    
        // Expect that at this point when the input value has less than 5 characters,     the button is still disabled.
        expect(addTodoButton.disabled).toBe(true);
    
        // Change the value of the input to have five characters
        todoInputElement.value = 'ABCDE';
        Simulate.change(todoInputElement);
    
        // Expect that at this point when the input value has 5 characters, the button is enabled.
        expect(addTodoButton.disabled).toBe(false);
      });
    });
    
    
    
    私たちのテストは、ユーザーがどのように実装されているかを保証します.
    ユーザーが実際にクリックするとき、ケースをカバーするためにさらに進みましょうAdd todo ボタン
    
    // App.test.js
    
    
    test('clicking the add todo button should save the new todo to the api, and display it on the list', async () => {
       const NEW_TODO_TEXT = 'OPEN_PULL_REQUEST';
       // Spy on getTodos function 
       const addTodoSpy = jest.spyOn(todoService, 'addTodo');
    
       // Mount the component
       const { container, getByTestId, queryByText } = render(<App todoService={todoService} />);
    
       // Wait for the promise that fetches todos to resolve so that the list of todos can be displayed
       await flushPromises();
    
       // Find the add-todo button and the todo-input element using their data-testid attributes
       const addTodoButton = getByTestId('todo-button');
       const todoInputElement = getByTestId('todo-input');
    
       // Change the value of the input to have more than five characters
       todoInputElement.value = NEW_TODO_TEXT;
       Simulate.change(todoInputElement);
    
       // Simulate a click on the addTodo button
       Simulate.click(addTodoButton);
    
       // Since we know this makes a call to the api, and waits for a promise to resolve before proceeding, let's flush it.
       await flushPromises();     
    
       // Let's find an element with the text content of the newly created todo
       const newTodoItem = queryByText(NEW_TODO_TEXT);
    
       // Expect that the element was found, and is a list item
       expect(newTodoItem).not.toBeNull();
       expect(newTodoItem).toBeInstanceOf(HTMLLIElement);
    
       // Expect that the api call was made
       expect(addTodoSpy).toHaveBeenCalled();
    });
    
    
    
    新しいヘルパー関数を導入した.queryByText , 要素が見つからなかった場合はNULLを返します.この関数は、新しいtodoが実際にtodosの現在のリストに追加されたかどうかをアサートするのに役立ちます.

    持ち帰り


    今、あなたの反応コンポーネント/アプリケーションのほとんどの統合テストを書く方法を見てきました.ここでいくつかの重要なヒントを取る:
  • あなたのテストは、ユーザーがどのようにアプリケーションと対話するかについてより多くの傾向があるべきです.例えば、状態変更をチェックしないようにしてください.
  • ベストプラクティスの場合は、レンダリングされたコンテナーのインスタンスを取得するのを避けるため、ユーザーはそれと対話しません.
  • 常に完全なレンダリングを実行すると、これらのコンポーネントが実際に実際の世界で正しく動作することをより自信を与える.本当の話は、コンポーネントは、これまで浅い現実世界にマウントされます.
  • このチュートリアルでは、単体テストの重要性の相違を目指していません.あなたのアプリケーションのテストを書くとき、あなたは考慮する良いガイドであるかもしれません.