TDDで開発すべき理由 Pourquoi il faudrait toujours développer en TDD

こんにちは、フロントエンドエンジニアのCouthouis Paulです。メンバーの皆からはクーさんと呼ばれています。

今回、TDDについてお話ししてみたいと思います。私はフランス出身なので、母国語のフランス語を少しまじえながら記事を書きました。 挿絵イラストは同じチームの山本さんが描いてくれています! よければ、ぜひご覧下さい。

introduction

一つに統制された開発手法によって仕様が実現されることが理想的ですが、現実的には開発手法は開発者と同じくらい多くあります。 そんな状況で、過去の開発手法によって仕様の変更に対応することが難しくなる、という問題に直面します。

そしてその変更自体がさらに次の変更を困難にしてしまい、保守がどんどん困難になるコードが引き継がれるという悪循環が発生します。 これは開発者にとってよく知られた問題です。

TDD : Test Driven Development

開発手法としてのTDDで特徴的に感じるのは「テスト」です。 ですからTestが注目され「TDDはテストのための手法である」と言われることがあります。

しかしそれは本質的ではありません。 TDDで一番注目すべき文字は、Test ではなく Drive なのです!

TDDにおいて、テストは開発にありがちな無理や徒労、脱線を防止してくれる、あなたのためのガイドなのです。

原理は「ベイビーステップ(小さなステップ)」です。

  • テストを書く
  • 最初はテストに失敗するコードを書く
  • できるだけシンプルにコードを修正し、テストが成功するように修正します(この時点でコードが美しい必要はありません)
  • テストを通過したら、充分に時間をかけて、コードを美しく、読みやすく、リファクタリングします。

これで次のステップに進むことができ、TDDの輪をさらに広げることができます。

これを実践することで、常にテストカバレッジが100%になります。 (そうでなければ、何かがおかしい)

メリットは他にもあります。

開発者が全体を慮り、予想・想像しながら開発し、結果的に迷走や蛇行運転となり、徒労となっている状態はよくあります(YANGI の原則から逸脱する)。

TDDは、現在の「ベイビーステップ」にのみ焦点を当てます。 いま行おうとしている次のステップは自ずと明らかになっていて、あらかじめ思考しておく必要もありません。

つまり、TDDはあなたのガイドであり、ドライバーでさえあるのです。 まるで、車のGPSが正しい方向か間違った方向かを教えてくれるように、アシストしてくれるのです。

それがTDDの醍醐味です。

TDD:Une discipline exigeante

しかしTDDを実践するには、厄介な問題があります。 TDDを成功させるためには、今までの習慣を捨て、いくつかの必須ルールを守る必要があるのです。

  • テストを合格するためだけに、コードを追加する
  • テストに合格するため以外のコードを書いてはならない。「必要になるかも」等の予想は無用です。
  • テストを失敗したままにしてはいけない。

また、テスト対象のレイヤーが他のレイヤーに依存している時に、記述することが難しくなります。 依存性の注入(DI)と Clean Architecture は、TDDに取り組むために知っておくべき重要な概念です。

お勧めは、TDDの鍛錬に役立つ様々な「KATA」がインターネット上にありますから、参照し練習することです。

※ 「KATA」・・・日本の武道などで伝統的に使われるKATAの意味で使っています。

Développer en TDD dans une base de code existante

また、既存のコードをベースにしてTDDを始めることに難しさを感じるでしょう。私が Baseconnect に入社したとき、まさにそんな状況でした。

ゼロから立ち上げるプロジェクトにTDDで取り組むことが理想的ですが、実際の現場にそんな機会はなかなか恵まれません。対峙しなければならないコードには、独自のテストが実装されていたり、テストが無かったりするのです。

私はTDD無しではやっていけないことに気づき、解決策を模索し続け、ようやくその方法に辿り着きました。 いわば、私が編み出した「KATA」です。

その方法とは最小限のコードを既存のコードに注入することです。

  • まず、新しいスタンドアロンモジュールにTDDを使用して新しいコードを実装します。
  • 完了したら、依存性注入の原則を使用して、このモジュールを既存のコードに注入します。

実際にやってみましょう!

TDDで書かれたコードはすべて「new_src」フォルダに、古いコードは「src」フォルダに収められています。

まずディレクトリ構成です。(Frontendの例で説明します) 古いコードは「src」フォルダに収められており、TDDで書かれたコードはすべて「new_src」フォルダに収められるものとします。 最終的に「src」のコードは「new_src」に移動することを目指します。

> new_src
> node_modules
> src

「new_src」フォルダは、独立したアプリケーションとして機能します。 Reactコンポーネントがあります。

export const Header: React.FC = () => {
  return (
    <x.div data-testid="header" display="flex" justifyContent="space-between">
      <div>
        <Responsibles />
        <TagManager />
      </div>
      <div>
        <StatusManager />
      </div>
    </x.div>
  )
}

また、Redux Toolkitのスライスもあります。

import { createSlice } from '@reduxjs/toolkit'
import { CurrentUserEntity } from './entities'
import { retrieveCurrentUser } from './use-cases'

const initialCurrentUser: CurrentUserEntity = {
  uuid: '',
  authorityName: null,
  branchOfficesStatusOptions: [],
  statusOptions: [],
}

export const currentUserSlice = createSlice({
  name: 'currentUser',
  initialState: initialCurrentUser,
  reducers: {},
  extraReducers: builder => {
    builder.addCase(
      retrieveCurrentUser.fulfilled,
      (_, { payload: { currentUser } }) => ({
        ...currentUser,
      })
    )
  },
})

もちろんテスト用ファイルも。

it('should retrieve status options', async () => {
    const {
      dispatchRetrieveCurrentUser,
      selectStatusOptions,
    } = retrieveCurrentUserSUT().withCurrentUser(fakeAdmin).build()

    await dispatchRetrieveCurrentUser()

    expect(selectStatusOptions()).toEqual<CurrentUserEntity['statusOptions']>([
      {
        label: 'label1',
        value: 'value1',
      },
      {
        label: 'label2',
        value: 'value2',
      },
    ])
  })

最後に、モジュール全体を構築するメインファイルを用意します。 実際のアプリケーションとの唯一の違いは、注入に使用するキーワード 'export' の存在です。

import { Provider as ReduxProvider } from 'react-redux'
import { storeQueries } from './api/queries'
import { AppShell } from './app/AppShell'
import { createStore } from './core/store'

const store = createStore(storeQueries)

export const MainShell: React.FC = ({ children }) => {
  return (
    <ReduxProvider store={store}>
      <AppShell>{children}</AppShell>
    </ReduxProvider>
  )
}

目標は「new_src」のファイルを「src」のファイルに注入できるようにすることです。 そのため、MainShellは "src "のAppShellコンポーネントにインジェクションされます。

import { ThemeProvider } from '@xstyled/styled-components'
import { QueryClientProvider } from 'react-query'
import { NotificationProvider } from '~/components/Notification'
import { queryClient } from '~/queries/queryClient'
import { theme } from '~/style/theme'
import { MainShell } from 'new_src/main'

export const AppShell: React.FC = ({ children }) => {
  return (
    <QueryClientProvider client={queryClient}>
      <ThemeProvider theme={theme}>
        <NotificationProvider>
          <MainShell>{children}</MainShell>
        </NotificationProvider>
      </ThemeProvider>
    </QueryClientProvider>
  )
}

これにより「src」フォルダ内のファイルであっても、すべてのアクションとreduxセレクタにアクセスすることができるようになります。 ただし、既存の「src」テンプレートにReactコンポーネントを注入する必要があることに変わりはありません。

import { Header } from 'new_src/app/design/Header/Header'
~
~
~
 <FormProvider {...methods}>
      <Box backgroundColor={'mainBg'} marginTop="13.5rem" px="1rem">
        <input name="currentFocus" ref={register} style={{ display: 'none' }} />
        <Header />
        ~
        ~

こうすることで、コードのどの部分が100%テストでカバーされているかが分かります。 また、コードをリファクタリングして「src」から「new_src」にコードを移せば、いつか100%TDDのコードを手に入れられるでしょう。

Conclusion

開発者の誰もが、膨大で理解し難い、またメンテナンスが困難な開発を経験しています。

でも、最初から最後まですべきことをシンプルに保ち、メンテナンスの問題も無く、アプリケーション開発を成功させる解決策があるのです。それがTDDです。

最初、TDDはとっつきにくい開発方法です。 しかし、一度マスターすれば、あなたとそのチームを幸せにすることができます。

納得できない?ぜひ一度試してみてください!!最後まで読んでいただきありがとうございました!

herp.careers