React-コンポーネントのユニットテストの方法


ユニットテストとは


一般的なテストは、ユニットテスト、統合テスト、機能テストのいくつかのタイプに分けられます.統合テストと機能テストは言うまでもありません.ユニットテストは、モジュール、関数、またはクラスに対して正確性検査を行うためのテスト作業であり、ここでのユニットはプログラム作業の最小作業単位である.ユニットテストは入力のみに依存し、余分な環境に依存しないでください.ユニットテストが多くの環境に依存している場合は、統合テストが必要かもしれません.
ユニットテストは、開発モデルに基づいて、次の2つに分類できます.
  • TDD、TDDはTest Drive Developmentを指し、明らかな意味はテスト駆動開発、つまり私たちはテストの角度からプロジェクト全体を検査することができます.大体の流れは、まず各機能点に対してインタフェースコードを抽象化し、それからユニットテストコードを作成し、次にインタフェースを実現し、ユニットテストコードを実行し、このプロセスをユニットテスト全体が通過するまで循環することである.
  • BDDとは、Behavior Drive Development、つまり行動駆動開発を指す.行動駆動開発は、ソフトウェアプロジェクトの開発者、QA、および非技術者またはビジネス参加者間の協力を奨励する敏捷なソフトウェア開発技術です.主にユーザーのニーズから、システムの行動を強調します.BDDは2003年にDan Northによって命名され、テストドライバ開発への応答として、検収テストや顧客テストドライバなどの限界プログラミングの実践を含む.

  • 現在私が接触したプロジェクトはすべてBDDで、国内の先端プロジェクトはユニットテストに対する重視度がそんなに高くなくて、TDDのこのような先にユニットテストを編纂するモードの応用は多くありません.
    しかし、私は本当に言いたいのは、高カバー率のユニットテストは、オンラインbug率が大幅に低下するたびに、コード再構築の基礎でもあることを保証することができます.多くの古いプロジェクトでは、開発者が退職し、新しく引き継いだ人は再構築することができず、徐々にチームの負担と呼ばれ、ラインオフできないのは、ユニットテストがないため、変更しても予測できないバグが発生するのを恐れているからだ.
    ユニットテストの作成原則、推奨参考https://github.com/mawrkus/js-unit-testing-guide

    Reactユニットテストフレームワーク


    ここでは2つの状況に分けられます.
  • あなたのコードはすべてnode環境で実行できます.ブラウザ環境は必要ありません.Jest+Enzymeを選択します.

  • JestはFacebookが発表したオープンソースのJasmineフレームワークに基づくJavaScriptユニットテストツールです.内蔵されたテスト環境DOM APIサポート、ブレークスルーライブラリ、Mockライブラリなど、Spapshot Testing、Instant Feedbackなどの特性も含まれています.
    EnzymeはAirbnbオープンソースのReactテストクラスライブラリで、簡潔で強力なAPIを提供し、jQueryスタイルの方法でDOM処理を行い、開発体験が非常に友好的です.オープンソースコミュニティだけでなく、React公式の推薦も受けた.
  • あなたのコードはブラウザ環境に依存しているので、Karma+Jasmine+Enzymeを選択することをお勧めします.

  • Karmaは、テストファイルを検索し、コンパイルし、断言を実行するためのテスタ、Angularチームの作品です.
    Jasmineは「私たちが期待しているものを手に入れたのか」という断言庫です.describe、expect、itのような関数を提供し、関数またはメソッドがトリガーされたリスナーがあるかどうかをリスニングします.

    Jest + Enzyme


    Jest


    Jestには、実際にはブレークスルーライブラリとランチャーが含まれています.断言ライブラリは書き込みユニットのテスト時に使用するインタフェースであり、Jestに内蔵されている断言ライブラリはJasmineであり、使用文法は以下の通りである.
    describe("A suite is just a function", function() {
      var a;
    
      it("and so is a spec", function() {
        a = true;
    
        expect(a).toBe(true);
      });
    });
    

    describeメソッドは、ユニットテスト(Suites)のセットを行い、it内部はテストのセットのいずれかのテスト(Specs)であることを示す.具体的な文法はhttps://jasmine.github.io/tutorials/your_first_suiteおよびhttps://jestjs.io/docs/en/api .
    Jestのrunnerは簡単に使用できます.configファイルを構成すればいいです.
    module.exports = {
      transform: {
        "^.+\\.tsx?$": "ts-jest"
      },
      testURL: "http://localhost",
      testRegex: "\\.(test|spec)\\.(jsx?|tsx?)$",
      testPathIgnorePatterns: [
        "/node_modules/"
      ],
      moduleFileExtensions: [
        "ts",
        "tsx",
        "js",
        "jsx"
      ],
      testResultsProcessor: "jest-junit",
      moduleNameMapper: {
        "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/__mocks__/file-mock.js",
        "\\.(css|scss)$": "/__mocks__/style-mock.js"
      },
      collectCoverage: true,
      collectCoverageFrom: [
        "**/*.{ts,tsx}",
        "!**/*.d.ts",
        "!**/node_modules/**"
      ],
      coverageDirectory: "/tests-coverage",
      coverageReporters: ["json", "html"]
    }
    

    その他の設定可能なjest configは、「https://jestjs.io/docs/en/configuration .

    Enzyme


    Enzymeの基本的な使い方:
    import {mount, configure} from 'enzyme';
    import * as Adapter from 'enzyme-adapter-react-16';
    
    import Widget from './Widget ';
    
    configure({ adapter: new Adapter() });
    
    describe('try to use Enzyme', () => {
      let wrapper;
      let config = {};
      beforeAll(() => {
        wrapper = mount(<Widget />);
      });
      it('should have rendered widget', () => {
        expect(wrapper.hasClass('container')).toEqual(true);
      });
    });
    

    Enzymeには他のインタフェースも用意されていますので、参照してください.http://airbnb.io/enzyme/docs/api/.

    Karma + Jasmine + Enzyme


    JasmineもEnzymeもすでに紹介されていますが、Karmaについてお話しします.Karmaは運転ユニットテストの運転器(runner)である.Webpackを実行することができ、大きなスペースを提供します.Karmaを実行するのもプロファイルです.ここでは例を挙げます.
    'use strict';
    const path = require('path');
    const fs = require('fs');
    const argv = require('yargs').argv;
    
    const KARMA_HOST_API_PATH = '/base/api';
    const LOCAL_API_PATH = './api';
    
    const api = argv.apiurl || process.env.JSAPI_URL || KARMA_HOST_API_PATH;
    
    const isAPIBuilt = getAPIBuildStatus(api);
    const files = getTestFiles(api);
    const proxies = getProxies(api);
    module.exports = (config) => {
      config.set({
        frameworks: [
          'jasmine',
        ],
    
        client: {
          // used in ./tests/test-main.js to load API from api
          args: [api, isAPIBuilt]
        },
    
        plugins: [
          'karma-jasmine',
          'karma-webpack',
          'karma-coverage',
          'karma-spec-reporter',
          'karma-chrome-launcher',
          'karma-phantomjs-launcher'
        ],
    
        files: files,
    
        preprocessors: {
          '**/*.+(ts|tsx)': ['webpack',
                      // 'coverage'
                    ],
        },
    
        // optionally, configure the reporter
        coverageReporter: {
          type : 'html',
          dir : 'coverage/'
        },
    
        webpack: {
          // karma watches the test entry points
          // (you don't need to specify the entry option)
          // webpack watches dependencies
          devtool: 'source-map',
          mode: 'development',
          output: {
            libraryTarget: "amd"
          },
          resolve: {
            extensions: [".ts", ".tsx", ".js", ".jsx"],
            alias: {
              'builder': path.resolve(__dirname, 'builder/')
            },
          },
          module: {
            rules: [{
              test: /\.tsx?$/,
              exclude: /node_modules/,
              use: [
                {
                  loader: 'ts-loader',
                  options: {
                    transpileOnly: true,
                    configFile: require.resolve('./tsconfig.json')
                  }
                }
              ]
            }, {
              test: /\.(scss|css)$/,
              use: [{
                loader: 'style-loader'
              }, {
                loader: 'css-loader',
                options: {
                  sourceMap: process.env.NODE_ENV === 'production'? false: true
                }
              }, {
                loader: 'postcss-loader',
                options: {
                  plugins: []
                }
              }, {
                loader: 'sass-loader',
                options: {
                  sourceMap: process.env.NODE_ENV === 'production'? false: true
                }
              }]
            }, {
              test: /\.(png|jpg|gif|svg|eot|ttf|woff|woff2)$/,
              use: {
                loader: 'url-loader',
                options: {
                  limit: 10000,
                  fallback: path.join(__dirname, './webpack/webpack-file-loader/main.js'),
                  outputPath: (rPath, fullPath) => {
                    return path.relative(__dirname, fullPath).replace(/\\/g, '/');
                  },
                  useRelativePath: true,
                  name: fullPath => {
                    return '../' + path.relative(__dirname, fullPath).replace(/\\/g, '/');
                  }
                }
              }
            }]
          },
          stats: {
            colors: true,
            reasons: true
          },
          externals: [
            function(context, request, callback) {
              if (...) {
                return callback(null, "commonjs " + request);
              }
              callback();
            }
          ]
        },
        proxies: proxies,
        proxyValidateSSL: false,
    
        reporters: ['spec',
                    // 'coverage'
                  ],
        mime: {
          'text/x-typescript': ['ts','tsx']
        },
    
        colors: true,
        autoWatch: false,
        browsers: ['Chrome'], // Alternatively: 'PhantomJS', 'Chrome', 'ChromeHeadless'
        browserNoActivityTimeout: 100000,
        customLaunchers: {
          chrome_without_security: {
            base: 'Chrome',
            flags: ['--disable-web-security'],
            displayName: 'Chrome w/o security'
          }
        },
        // Continuous Integration mode
        // if true, it capture browsers, run tests and exit
        singleRun: !!argv.singleRun
      });
    };
    
    function getAPIBuildStatus(api){
      if(isUsingKarmaHostApi(api)){
        return fs.existsSync(path.join(LOCAL_API_PATH, 'init.js'));
      }else{
        return !/\/src(\/)?$/.test(api);
      }
    }
    function getTestFiles(api){
      let tests = [
        './tests/test-main.js',
        {pattern: path.join(argv.root, '/**/*.apitest.+(js|ts|jsx|tsx)'), included: false}
      ]
      if(isUsingKarmaHostApi(api)){
        tests = tests.concat([
          {pattern: './api/**/*.+(js|html|xml|glsl|gif)', included: false, watched: false}
        ]);
      }
    
      return tests;
    }
    function getProxies(api){
      if(isUsingKarmaHostApi(api)){
        return {};
      }else{
        return {
          '/base/api/': {
            'target': api,
            'changeOrigin': false
          }
        }
      }
    }
    
    function isUsingKarmaHostApi(api){
      return api.indexOf(KARMA_HOST_API_PATH) > -1;
    }
    
    

    この構成により、ユーザは、コマンドラインパラメータ--apiurlを介して、lodashのcdn urlのような現在のユニットテスト依存api urlを構成することができる.このパラメータが指定されていない場合、ルートディレクトリの下に./api/があり、このディレクトリの下にあるすべてのファイルがhostされます({pattern: './api/**/*.+(js|html|xml|glsl|gif)', included: false, watched: false}を介して).urlの提供とurlの提供がないため、リクエストは相対パスが同じですが、urlドメインが異なるため、urlを使用する場合はエージェントを有効にする必要があります(proxies: proxies).
    ユニットテストを実行する前に、いくつかの構成をロードする必要があります.ここでは./tests/test-main.jsで実行し、scriptラベルで事前にロードします.test-main.jsでは、node端に注入される変数はwindow.__karma__.config.argsによって得られ、注入変数はargs: [api, isAPIBuilt]によって得られる.
    その他の構成は、https://karma-runner.github.io/2.0/config/configuration-file.html .

    その他のテストフレームワークとまとめ


    internも使ったことがあります(https://theintern.io/docs.html#Intern/4/docs/README.md)を使用して、ユニットテストフレームワークも用意されています.現在intern v 4のdocはまだ完全ではありません.coverageとreportを構成するのは少し苦労しています.例えば、junit reportを構成するには、ソースコードの中でどのような構成項目があるのか、どのように使用するのか、説明しません.
    まとめるのも簡単ですが、Reactユニットテスト、構文、mockデータを書くには、Jasmine構文とEnzyme構文を熟知する必要があります.Jasmineは断言ライブラリを提供し、Enzymeは仮想的なrenderコンポーネントを提供し、ライフサイクルをトリガーすることができます.ユニットテストを実行するには、ブラウザ環境に依存せず、またはブラウザへの依存ではなくmockデータでスキップできる項目を実行する必要があります.Jest、ブラウザ環境に依存し、mockデータもスキップできない項目を使用することをお勧めします.Karmaを使用することをお勧めします.