Zaimの徳元です。この記事はくふうカンパニー Advent Calendar 2019の12日目の記事となります。
ReactやTypeScriptの経験が浅い僕がプロジェクトでこれらを使うことになった時に、品質や保守性が一定以上担保されるようなコードを書くために考えたことについて書いていこうかと思います。
「品質」と「保守性」
僕は品質と保守性という言葉をそれぞれ以下のようにざっくりとまとめました。
- 品質
- 正しく動く、バグがない
- 動作が速い
- 保守性
- テストが書きやすい
- コードの理解がしやすい
- 変更がしやすい
この品質・保守性と、実装のスピードのバランスを取るために、僕はコンポーネントの設計方針としてAtomic Designを取り入れ、自動テストを効率を考えつつ限定的に取り入れることにしました。
Atomic Designとは
Atomic Designというのは、UIを構成する要素を原子に見立てて実装していく、UI設計手法の1つです。Atomic Designを説明する際によく出てくるのは以下の図です。
(参考:http://atomicdesign.bradfrost.com/)
Atomsという最小単位のコンポーネントを組み合わせ、最後にPages(実際のページ)までつくります。
ここからは、それぞれの階層をかんたんに説明していきます。
Atoms
UIコンポーネントとしての最小単位となります。コンポーネントには何らかの機能が必要です。なので、Atomsはそれ以上UIとしての機能性を破壊しない最小要素とも言えます。
例:インプット、ボタン、テキスト、ヘディング、バルーン、カード、バッジなど
Molecules
Atomsを組み合わせて、ユーザーの動機に対する機能の単位でUIをコンポーネント化するのがMoleculesです。
例えば、検索フォームもそうです。「何かキーワードで検索したい」という動機に対する機能を提供します。ボタン、テキスト・インプット、テキストの3つのAtomsを組み合わせて作れます。
Organisms
MoleculesやAtoms、時にはOrganisms層のコンポーネント自体を使って作られるコンポーネント。
Moleculesはユーザーの関心ごとに対して機能を提供しましたが、Organisms層はコンポーネントで完結するコンテンツを提供します。
例えば、「いいね」を押してカウントアップするだけのMoleculeを、プロフィールアイコンコンポーネントや記事コンポーネントと一緒に置くことによって「リアクションできるユーザーの投稿記事」という独立したコンテンツ(Organisms)をつくることができます。
Templates
Organisms, Molecules, Atomsなどのコンポーネントを実際のサービスと同様に配置するための層です。この層の目的はコンポーネントがページ上で正しくレイアウトされるかを確認することです。レイアウトのテストをする際にはこのコンポーネントを利用したりします。
Pages
コンポーネントが所属する層ではなくプロダクトそのものです。Templatesに実際のデータを流し込んだものとなります。
うれしいこと・困ること
いま僕はこのAtomic Designに沿ってコンポーネントを実装していますが、うれしいことがある反面、困ることもあったりします。
うれしいこととしては、(こういった設計方針ですから当たり前ですが)自然と再利用性の高いコンポーネントを作ることができるようになることです。また、設計方針がわかることでそれをベースとした議論ができます。
こういった方針がないと「これは別コンポーネントに切り出した方がいいんじゃない」くらいの議論しかできなかったりします。
困ることは、いま自分が作るコンポーネントがどの層に属すのか、判断に迷うことがあることです。ググったらわかると思いますが、Atomic Designを採用している他の企業のエンジニアさんたちもそこで色々困っていて、それぞれの組織でコンセンサスを取って進めているようです。
Container ComponentとPresentational Component
Atomic Designに則った粒度でコンポーネントを作るとしても、それぞれのコンポーネントも、そのコンポーネントの関心(やっていること)によって分けることができます。それがContainer ComponentとPresentational Componentです。
Container Componentは主に「どう機能するか」に関心を持ち、データを取得・加工・整形した後にPresentational Componentにデータをpropsとして渡します。
Presentational Componentは主に「どう見えるか」に関心を持ち、DOMマークアップをふんだんに持ちます。
このようにコンポーネントの関心を分けることによって、些細な見た目の違いを持ったコンポーネントを作る際にはContainer Componentを再利用することができます。
const Container: FC<ContainerProps> = ({ addNumber, addedNumber, presenter }) => {
const sum = addedNumber + addNumber
presenter({ sum })
}
const Presenter: FC<PresenterProps> = ({ sum }) => <p>{sum}</p>
const UnderlinedPresenter: FC<{ sum: number }> = ({ sum }) => <p className="underlined">{sum}</p>
const UnderlinedComponent: FC<Props> = ({ addNumber, addedNumber }) => {
return <Container addNumber={addNumber} addedNumber={addedNumber} presenter={presenterProps => <UnderlinedPresenter {...presenterProps} />} />
}
テスト
次に、上記の設計方針で作ったコンポーネントをどのようにテストしていくかについて書いていきます。冒頭に述べた通り、品質と保守性を担保するために、主に以下のテストを書いていくことにしています。
- ロジックテスト
- インタラクションテスト
- E2Eテスト
テストを書くことのメリットは、品質の面ではコンポーネントレベル・そしてサービス全体レベルでの動作がある程度担保されること、保守性の面ではテスト自体がドキュメントの役割を果たすこと、テストの書きやすさなどでプログラムのリファクタに繋がることなどが挙げられそうだと思っています。
ロジックテスト
ロジックテストは、先ほど説明したContainer Componentが役割として持っているロジックの部分をテストします。Jestを使います。
先のコードを見て気づいた方もいると思いますが、主に僕が所属しているプロジェクトではクラスコンポーネントではなく関数コンポーネントで書いているので、戻り値を確かめるためにpresenterのpropsを見る形でテストを行います。
describe('HogeContainer', () => {
const presenter: FC<PresenterProps> = props => {
return <React.Component {...props} />
}
it('returns sum', () => {
const { sum } = Container({ 1, 3, presenter })
expect(sum).toBe(4)
})
}
インタラクションテスト
インタラクションテストでは、ユーザーのインタラクションをシミュレートし、チェックします。Enzymeを使ってShallowレンダリング(子コンポーネントはモックに置き換えてレンダリングする)をし、Injectした関数などが呼ばれることをテストします。
describe('HogeComponent', () => {
it('callbacks on mouse functions', () => {
const mockMouseEnter = jest.fn()
const mockMouseLeave = jest.fn()
const wrapper = mount(
<Hoge
onMouseEnter={mockMouseEnter}
onMouseLeave={mockMouseLeave}
/>
)
wrapper
.find('div span')
.at(1)
.simulate('mouseenter')
wrapper
.find('div span')
.at(1)
.simulate('mouseleave')
expect(onMouseEnter.mock.calls.length).toBe(1)
expect(onMouseLeave.mock.calls.length).toBe(1)
})
})
E2Eテスト
E2Eテストはjest-puppeteerを使うことにしています。
const timeout = 5000
describe('/ (Home Page)', () => {
let page
beforeAll(async () => {
page = await global.__BROWSER__.newPage()
await page.goto('https://google.com')
}, timeout)
it('should load without error', async () => {
const text = await page.evaluate(() => document.body.textContent)
expect(text).toContain('google')
});
},
timeout,
)
参考:https://jestjs.io/docs/ja/puppeteer
さいごに
急ぎ足でしたが、経験が浅い僕のようなエンジニアでも品質や保守性を一定数担保するためのしくみとしてのAtomic Designやテストについて書かせてもらいました。
時間がないため詳しくは書けませんでしたが、テストを書くと一言でいっても、何も考えずに逐一書いていると二重実装している気分になる時もあります。この辺りは経験を積んだりしながら見極められるようになれれば、スピードと品質・保守性のどちらも高めていけそうだと思っています。
僕の記事はこれでおしまいです。
明日は tatsuo48 さんの記事となります!