iimon TECH BLOG

iimonエンジニアが得られた経験や知識を共有して世の中をイイモンにしていくためのブログです

実録!別れて幸せになる方法(あくまでもコンポーネント設計の話

はじめに

こんにちは!4月にジョインしましたマツダと申します。

これまではどちらかというとバックエンドの開発を主1にやっていたのですが、 iimonに入ってからはフロント中心に担当させていただいています。

これまではTDD(テスト駆動開発)での開発経験もあって、自動テストがもたらしてくれる安心感に身を委ねる開発スタイルにすっかり慣れてしまっていたのですが、今の担当プロダクトではテストが書かれているのは一部に留まっているようでした。

また、コンポーネント設計はatoms,molecules,organismsからなる、いわゆるAtomic Designを採用しているのですが、見た目とロジックが混ざっている部分も多く、どこをどうテストすべきか頭を悩ませています。

当然、リファクタリングしたい欲も高まるわけですが、テストがないと、リファクタリング後にロジックを壊していないことが保証できないのでなかなか難しい問題です。

そんなわけで、今回は私の過去の経験から、こんなふうにコンポーネント設計するとラクにテストが書けるよ、という方法を1つ紹介してみたいと思います。

Container / Presentationalパターン

1つのコンポーネントを、「Containerコンポーネント」と「Presentationalコンポーネント」に分けることでさまざまなメリットを享受できます。

「Presentationalコンポーネント」は見た目にだけ責務を持って、見た目を変化させるデータや状態(ステート)は持ちません。

「Containerコンポーネント」はデータの取得や状態管理を行い、Presentationalコンポーネントにデータを受け渡すのが責務です。

説明するより、実際に見ていただいたほうがもわかりやすそうですね。たとえば以下のコード・・・シンプルではありますが、見た目とロジックが混ざっていて、既にテストしにくそうです。

const UserProfile = () => {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch('/api/user')
      .then(response => response.json())
      .then(data => {
        // !! おっと、ロジックが混ざっているぞ
       const transformedUser = { ...data, name: data.name.toUpperCase() };
       setUser(transformedUser);
      });
  }, []);

  return (
    <div>
      {user ? (
         <div>
           <p>{user.name}</p>
           <p>{user.lv}</p>
         </div>
      ) : (
         <div>NOW LOADING...</div>
      )}
    </div>
  );
};

この例でもテストできないわけではないですが、いろいろ面倒なことがあってなんだか憂鬱です・・・。

  • API呼び出し部分をモックしなきゃ・・・
  • useEffectの中に名前を加工するロジックが混ざっとる・・・

では、これをContainer / Presentationalパターンで書き直してみます。

まずは Presentionalコンポーネント

const UserProfile = ({ user }) => {
  <div>
    <p>{user.name}</p>
    <p>{user.lv}</p>
  </div>
};

清々しいまでに見た目だけ。これにはStorybook先輩もニッコリです。・・・たぶん。しらんけど(笑)

そしてContainerコンポーネント

import UserProfile from './UserProfile';

const UserContainer = () => {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch('/api/user')
      .then(response => response.json())
      .then(data => setUser(data));
  }, []);

  return user ? <UserProfile user={user} /> : <div>NOW LOADING...</div>
};

こんな感じですかね。読込中の見た目を持ってるじゃないか、というツッコミが入りそうですが、そこは将来Loadingコンポーネントを作るということで一旦見逃してください(汗)

あ、肝心の 名前を大文字にする のロジックを忘れていました・・・。

ロジックは独立した関数として定義しましょう。置き場はその時に応じて考えるのがいいと思いますが、今回は便宜上utils.jsに置くことにします。

// utils.js
export const transformUserName = (user) => {
  return { ...user, name: user.name.toUpperCase() };
}

今回はあくまでも例ということで至極単純なロジックですが、普段皆さんが書いている複雑なロジックを思い浮かべてください・・・。とりあえず、何をおいてもいちばんにテストしたいのはこのロジックのはずです。

import { transformUserName } from './utils';

test('ユーザ名が大文字変換されること', () => {
  const user = { name: 'Iimon Taro', lv: 99 };
  const transformedUser = transformUserName(user);
  expect(transformedUser.name).toBe('IIMON TARO');
});

このように、何も考えることなく、憂鬱になって屋上で川や高速道路を行き交う車を見ながら黄昏れることもなく、ロジックのテストを書くことができました。

まとめ

見た目にだけ責務を持つ「Presentationalコンポーネント」と、データや状態を持つ「Containerコンポーネント」に分割することで見た目とロジックを分離して、テストもサクッと書けてハッピーになれるよ、というお話でした!

今回は触れませんでしたが、Storybookを使ったコンポーネントのカタログ化やUI側のテストにも効果的なはずですので、ぜひ1度お試しくださいませ。

さいごに

文中に出てくるコードは雰囲気で書いただけで、動作確認も何もしておりません。もし恥ずかしい間違いに気づいた方はこっそり耳元で囁いてください。

弊社ではエンジニアを募集しております!

ぜひカジュアル面談でお話ししましょう!

ご興味ありましたら、以下のリンクからご応募ください!! Wantedly / Green


  1. 以前の会社で、人がいないのでしょうがなく泣きながらVueを書いていたことはありました(笑)