テスト駆動開発再入門

基本的なことをおさらい

  • テストの目的
  • テストの種類
  • テストの構成要素
  • テスティングライブラリ
  • テスティングライブラリの選定方法

テストの目的

・プロダクト品質の継続的担保

・生産性の向上

・プログラマーの心理的安全性

ぱっとあげるとすれば・・・

プロダクト品質の担保

プロダクト品質の向上ではなく

テストがあることによる一定の継続的な品質の担保かなと

後はプログラマーの心理的安心感とか

生産性の向上

疎結合システムによるテスタビリティの向上

 
// deprecated
function A(val: string){
    console.log(val)
    return val
}

// Recommendation
function A(val: string){
    return val
}
function test_A(){
    const received = A("hoge")
    const expected = "hoge"
    expect(received).toBe(expected)
}

// deprecated
function main(price: number){
    if (price >= 100) {
        // 何か処理
    }
}


// Recommendation
function main(price: number){
    function price_is_higher_than_100(price: number){
        return price >= 100
    }
    
    if (price_is_higher_than_100(price)) {
        // 何か処理
    }
    

}
function test_price(){
    expect(price_is_higher_than_100(100)).toBe(true)
    expect(price_is_higher_than_100(99)).toBe(false)
}

not log allow info

 

テスタビリティな一例

 

テスタビリティが向上すると 

  • コードを部分的に検証可能(疎結合)
  • 他のコードの振る舞いを考える必要がない(スコープの最小化)
  • テストコードを記述する分コードが増えるが、プログラマーは小さい単位でシステムを組み立てることが可能となる。
  • 疎結合とスコープの最小化による部分理解でのみ対応が可能となる(考えることが減ることは良いこと)

テストの種類

・単体テスト

・結合テスト

・E2Eテスト

・受け入れテスト

コンポーネントテスト

・UIテスト

・スナップショットテスト

など・・・・

単体テスト

一番基本となるテスト

まずここからテストを書く

// deprecated
function A(val: string){
    console.log(val)
    return val
}

// Recommendation
function A(val: string){
    return val
}
function test_A(){
    const received = A("hoge")
    const expected = "hoge"
    expect(received).toBe(expected)
}

結合テスト

単体テスト済みの各クラス群をまとめてテスト

コントローラーとかルーティングとか

// system_ruote.ts

// ライブラリーのインポート
import rote from "rote"

// リポジトリーのインポート
import Repository from "Repository"

route::get('/api/news/', async () => Repository::getNews())

export default route

// test.ts
import route from "system_ruote"
import mokcRoute from "mock"

test('apiのnewsのルーティングを確認', () => {
	const _route = mockRoute(route)
    
   const response =  _route.get('/api/news/')
   expect(200).toBe(response.status)
   expect([{name: 'ニュースタイトル'}]).toBe(response.data)
	
})

E2Eテスト

システムをUIからまるっとテスト

nightwatchを使った例

const _ = require('lodash')
const TableCellSection = require('page_objects/project/list/tableCell').alias

module.exports = {

  /**
   * ログインを行う
   * @param browser
   */
  before: function (browser) {
    browser.login(browser)
  },

  /**
   * テストが走るたびに進行中のタブがアクティブになるように
   * @param browser
   */
  beforeEach: function (browser) {
    browser.page.project.list.searchBox().activeProgressTab()
  },

  /**
   * ブラウザ閉じる
   * @param browser
   */
  after: function (browser) {
    browser.end()
  },

  'プロジェクト名で検索ができていることを確認': function (browser) {
    let name = 'ア'

    browser.page.project.list.searchBox().setProjectName(name)
    browser.page.project.list.tableCell().eachText(TableCellSection.ELEMENTS.PROJECT_NAME, text => {
      browser.assert.ok(text.value.match(new RegExp(name)))
    })

    browser.step()
  }
}

受け入れテスト

最終納品要因を満たしているかを確認するテスト
単体テスト・結合テスト・E2Eテストなどで満たせる場合もあれば、満たせない場合もある。

 

その場合は、受け入れテスト仕様書を用意し、手動で最終確認をする。
 

その他

テストの構成要素

  • Fixture
  • Mock
  • Spyon

Fixture

  • テスト開始するために必要なもろもろの構成物

テストコードの8割はFixtureを用意する作業(肌感)

  • テストを開始する時に必要なもの
    1. データベースの初期化
    2. APIのモック化
    3. 検証データの用意
    4. ブラウザの起動
    5. Vuexの初期化など
  • テスト完了時に必要なもの
    1. データベースの終了
    2. Mockのリストアなど

Fixtureの用意

テストコード

8割

2割

Mock

  • テスト実行時にテストに関係のない処理を置き換えること
  1. ルーティングのテストでのリポジトリー
  2. フロントの処理でのAPI
  3. VueコンポーネントでのVuexなど (実際のコード)

テストの範囲が広くなるとMock対象のオブジェクトが増えるので夫々に対応する必要があり、Mock化は少しテストの難易度が上がる

Spyon

  • Mock対象のオブジェクトの中でも特にメソッドを置き換えたい場合に使う

import * as utils from '../../src/utils'
import { mockGetNuxt, mockGetBuilder, mockGetGenerator, NuxtCommand } from '../utils'

describe('build', () => {
    let build

    beforeAll(async () => {
        build = await import('../../src/commands/build').then(m => m.default)
        jest.spyOn(utils, 'forceExit').mockImplementation(() => {})
    })

    afterAll(() => {
        process.exit.mockRestore()
    })

    afterEach(() => jest.resetAllMocks())

    test('build can set force exit explicitly', async () => {
        mockGetNuxt()
        mockGetBuilder(Promise.resolve())

        const cmd = NuxtCommand.from(build, ['build', '.', '--force-exit'])
        await cmd.run()

        expect(utils.forceExit).toHaveBeenCalledTimes(1)
        expect(utils.forceExit).toHaveBeenCalledWith('build', false)
    })

})

exit関数のテストなど

テスティングライブラリ

Javascriptに絞ると・・・・

テスティングライブラリの選定方法

  • テストの対象は?
  • テストの(自身の)スキルは
  • コードの対象は?

テストの対象は?

 

UIテスト

単体テスト

E2Eテスト

などテスト対象によって

Awesomeから選定する

テストのスキルは?

テスト初心者なら全部入りのJestが楽。

テストの経験が豊富ならAvaやsinonなど組み合わせると良い

 

テストの対象は?

ライブラリコードならAva

ドメインを含んだアプリケーションならJest

ワークショップ

  • truthyな単体テスト
  • console.logをspyonする
  • ドメインテスト(Storeのテスト)
  • カバレッジの計測
 

アドバンスト

  • テストの実行タイミング
  • CI/CDの展開と自動テスト
  • テスト方法の情報源

テストの実行タイミング

  • 保存時 - watch
  • コミット時 - husky
  • Push時 - hook
  • プルリクエスト時 - hook
  • マージ時 - hook
  • CI / CD時 - circle ci

様々な開発フロー上で組み込むことが可能

CI/ CDの展開

CI / CDサービスを使うことにより、テストを外部サーバ上で自動で検証することが可能

Circle CIの画面

テストの情報源

質問タイム

次回予定

  • Taiwindを選択した理由
  • CSS 2019サーベイによるTailwindの位置
  • ユーティリティファーストとdotCSS
  • NuxtへのTailwind導入方法
  • Vueコンポーネントとの相性
  • 各種Tailwindの事例
  • tailwindのベストプラクティス
  • Tailwind cssコミュニティの紹介

モダンWeb開発1 Tailwind + Nuxt

Thanks.

テスト駆動開発再入門

By Yutaka Wakai

テスト駆動開発再入門

  • 586