Wiz テックブログ

Wizは、最新のIoTやICTサービスをお客様に届ける「ITの総合商社」です。

TestCafe v1.14.0がリリースされました

こんにちは、フロントエンドエンジニアの菅野です。

4月7日にリリースされたv.1.14.0で待望の機能が追加されました!

今回はリリースノートをもとにアップデート内容とそれに伴いTestCafeを実装しているプロジェクトのリファクタリングを行なったので、ご紹介したいと思います。

E2Eの取り組みについて書いた記事もありますので興味がありましたらそちらもぜひ。

アップデート内容

スクロールアクションの機能強化

以前のバージョンでは、ページスクロールをするためにClientFunctionを使い、画面外のDOM要素を操作する必要があったのですが、 今回のリリースで専用のスクロールアクションが導入され簡単にスクロールできるようになりました。

以前までのスクロール処理

export const scrollBottom = ClientFunction(function() {
  const elementHtml = document.documentElement
  const bottom = elementHtml.scrollHeight - elementHtml.clientHeight
  window.scroll(0, bottom)
})

ちょっとめんどくさい感じで処理を書いていました・・・

便利なメソッドがたくさんある中で「なんでスクロールはないんだろう?」と不思議に思っていたので個人的に嬉しい機能追加です!

追加されたスクロールアクションは3つ

t.scroll :要素を指定された位置にスクロールします

t.scrollBy :指定されたピクセル数だけ要素をスクロールします

t.scrollIntoView :要素をスクロールして表示します

今回は、t.scrollアクションを使い、リファクタリングを行いました。

リファクタリング後のスクロール処理

const container = Selector("footer")
await t.scroll(container, 'bottom')

これだけです!

要素内のスクロールできる高さから要素の高さを引いて(ゴニョゴニョ...)しなくても良くなり、直感的な見やすいコードになりました!

その他のt.scrollByt.scrollIntoViewの使い方はこのようになっています。

t.scrollBy

設定されたピクセル数だけターゲットをスクロールします。 例ではWebページを上に200px、右に500pxスクロールさせた場合です。

fixture`Scroll Action`
  .page('http://example.com')
test('Scroll the webpage', async t => {
  await t
    .scrollBy(500, -200)
})

t.scrollIntoView

要素をスクロールして表示します。 (使いどころがピンと来てないのですが、スクロールアニメーションがある要素に使うと良いのでしょうか?)

fixture `Scroll Actions`
  .page `http://www.example.com/`
test('Scroll element into view', async t => {
  const target = Selector('#target')
  await t
    .scrollIntoView(target)
})

おまけ

サイトもいつの間にかリニューアルされていました!

リニューアル前 リニューアル前

リニューアル後  リニューアル後

最後に

Wizではエンジニアとして一緒に働く仲間を絶賛募集しております。

ご興味のある方、是非ご覧下さい..!!

careers.012grp.co.jp

Next.jsでのDynamic Importの使い所

こんにちは、フロントエンドエンジニアの髙橋です。

最近社内でNextを採用したプロジェクトが増えつつあるなか、色々な機能に驚き、助けられている日々を過ごしております。 そんな中、今回はDynamic Importの使い所について考えてみたいと思います。

そもそもDynamic Importって??

その名の通り動的なインポートのことです! ES2020の新機能で、Nextでもサポートされています。

書き方としては、Nextが提供しているnext/dynamicからimportして下記のように使用します。

import dynamic from 'next/dynamic'

// 静的なインポート
// import StaticComponent from '../components/hello'

// 動的なインポート
const DynamicComponent = dynamic(() => import('../components/hello'))

function Home() {
  return (
    <div>
      <Header />
      <DynamicComponent />
      <p>HOME PAGE is here!</p>
    </div>
  )
}

export default Home

nextjs.org

どうやって使用する??

そんなDynamic Importですが、さまざまな使い方があると思います。

今回は私がよく使う2つの用途を書いていきます!

パフォーマンス改善に使用する

一つ目の用途はパフォーマンス改善です。

例えば、このようなユーザーの操作によって切り替わる要素があるとします。

 

静的インポートの場合

こちらを静的にImportするとこのように書けば表現できます。

import { useState } from 'react';
import List from '../components/List'
import ListSecondary from '../components/ListSecondary'

const IndexPage = () => {
  const [toggle, setToggle] = useState(true);
  return (
    <Layout title="Home | Next.js + TypeScript Example">
      <h1>Hello Next.js 👋</h1>
      <button onClick={() => setToggle(!toggle)}>
        Toggle Component
      </button>
      {toggle ?  <List /> : <ListSecondary />}
    </Layout>
  )
}

export default IndexPage

listlistSecondaryToggle Componentを押すことで切り替えるようにしています。

早速Light houseのスコアを見てみましょう。

シンプルなページなのに少しパフォーマンスが悪いですね… 原因はListSecondaryは初期画面に表示されないのにロード時に読み込んでしまっているためです。 (わかりやすくするためにListSecondaryはめっちゃ大きくしています…笑) ではこちらを動的にインポートすればどうなるでしょうか。

動的インポート(Dynamic Importの場合)

Dynamic Importの場合、以下のように書きます

const List = dynamic(() => import( '../components/List'))
const ListSecondary = dynamic(() => import( '../components/ListSecondary'), { loading: () => <p>loading...</p> })

const AboutPage = () => {
  const [toggle, setToggle] = useState(true);
  return (
    <Layout title="Home | Next.js + TypeScript Example">
      <h1>Hello Next.js 👋</h1>
      <button onClick={() => setToggle(!toggle)}>
        Toggle Component
      </button>
      {toggle ?  <List /> : <ListSecondary />}
    </Layout>
  )
}

挙動を確認してみましょう。

Listは最初から表示されているのに対して、ListSecondaryは最初に切り替わるとき、少しラグがあります。 (一瞬なのでかなり見えづらいですが…) これはToggle Componentを押したときにImportしているため、このような挙動になります。

ではこちらのスコアはどうでしょうか。 f:id:sotq17:20210519170107p:plain

少しスコア上がったことが確認できました!

静的にインポートしたときに比べ、Dynamic Importは初期ロード時に読み込む量を減らすことができるため、サイトスピードの向上に使用することができます。

Dynamic Importを使用することで、比較的簡単にパフォーマンスチューニングすることができます。 初期表示の際に不必要なComponentが多い場合はDynamic Importを検討してみてはいかがでしょうか。  

SSR回避に使用する

もう一つ私がよく使用する用途は、SSR回避の用途です。

Nextを使っているとこんなエラーが出てくる時はないでしょうか?? ReferenceError: alert is not defined

私は気を抜いているとよくこのように怒られてしまいます。。 NextはSSR前提で動いているので、こういったブラウザの機能を使おうとするとエラーが出てしまいます。

ちなみにこういった書き方だと、上記のようなエラーとなります

import React from 'react'

import Alert  from '../../components/Alert'
const AlertPage = () => {
  return (
    <Alert/>
  )
}
export default AlertPage


*Alertはalertの機能を持つReact Componentです。

こういうときにもDynamic Importが役に立ちます!

先程のDynamic Importの書き方に{ssr: false}というオプションを渡してあげるだけで、「このファイルはSSRしない」とNextが判断するので、エラー回避することができます。

先程のソースを変更すると、以下のようになります。

import React from 'react'
import dynamic from 'next/dynamic';

const Alert = dynamic(() => import( '../../components/Alert'),{ssr:false})
const AlertPage = () => {
  return (
    <Alert/>
  )
}
export default AlertPage

これでエラーがなくなるはずです!

参考までにソースが上がったGithubも載せておきます。

github.com

まとめ

今回はNextのDynamic Importの使い方についてご紹介しました。 今回ご紹介した用途は一部の使い方で、もっといろんなことができると思います。 こんな風に使えるよ!ってご意見あれば是非ご教示いただきたいです…!

最後までお読み頂きありがとうございました!

最後に


Wizではエンジニアとして一緒に働く仲間を絶賛募集しております。

ご興味のある方、是非ご覧下さい..!!

careers.012grp.co.jp

REST APIとは?ざっくりと理解してみる【初心者向け】

image.png

こんにちは、バックエンドチームの中嶋です。

まだ入社して2ヶ月ほどですが、先日にWeb技術の勉強会の機会を頂き「REST API」についてLTを行ったので、簡単に内容をまとめていきたいと思います。

 

あくまで基本的な概念をざっくりと理解する、初学者向けの内容です!

それでは順番にみていきましょう〜。

まずはRESTを知る

REST APIの前にそもそも「REST」とは何かというと、「シンプルなWebシステムの設計思想」のことです。


これは「REpresentational State Transfer」
(リプレゼンテーショナル・ステイト・トランスファー

の略で、

  • Representational →具象化された
  • State →状態の
  • Transfer →転送

つまり、ざっくりと理解すると

「具体的に状態を定義した情報のやり取り」

のような意味合いになります。

これについて「RESTの4原則」というものがあり、これを満たすものを「RESTfulなシステム」と呼んだりします。

RESTの4原則とは?

このRESTの4原則は、
HTTPを設計した中心人物であるRoy Fielding氏が2000年に提唱したもので、
 ①統一インターフェース
 ②アドレス可能性
 ③接続性
 ④ステートレス性

 の4つです。

 

順番に見ていきましょう。

①統一インターフェース

統一インターフェースは、あらかじめ定義・共有された方法でやり取りされることです。
Webの場合、例えば「GET・POST・PUT・DELETE等のHTTPメソッドでやりとりするよ~」とか「やり取りするデータはJSON形式にしようね〜」といったものになります。
データの形式にはXML等もありますが、近年は軽量なJSON形式が使われることが多いです。
image.png

②アドレス可能性

アドレス可能性は、全ての情報が一意なURI(識別子)を持っていて、提供する情報をURIで表現できることです。
このURIUniformed Resource Identifierの略で、Webの場合は通常URLで与えられます。
image.png

③接続性

接続性は、やりとりされる情報にはハイパーリンクを含めることができる、というものです。
1つのリンクから別の情報にリンク(接続)することができて、RESTfulなシステム同士なら円滑に情報連携を行うことができます。

image.pngimage.pngimage.png

④ステートレス性

ステートとは「状態」のことでしたね。これがレスなので「状態がない」ということになります。
つまり、RESTでは「やりとりが1回ごとに完結する」と理解することができます。

 

これは、前のやり取りの結果に影響を受けないのでシンプルな設計にできるというメリットがあります。(セッション管理が不要など)

 

WebにおけるRESTの4原則を図にすると..

あくまでひとつの例ではありますが、こんな感じでイメージできると思います。

image.png
 ①HTTP通信で,JSON形式でやりとりするよ →統一インターフェース
 ②このURLの情報くださーい →アドレス可能性
 ハイパーリンクを含んだページをあげるよ →接続性
 ④さっきの情報をくださいと言われても〜 →ステートレス性

じゃあ本題のREST APIとは??

「REST」についてざっくりと理解したところで、本題のREST APIについて見ていきま しょう。

まずAPIとはApplication Programing Interfaceの略で、ざっくりと、「プログラムの情報をやり取りする蛇口のようなもの」と理解することができます。
image.png

そして「REST API」とは先ほどのRESTの4原則に則ったAPIのことです。

 

ちなみに..
RESTful APIREST APIは呼び方の違いで意味はほぼ同じ。
またWeb分野では単にWeb APIと呼ぶこともある。

外部APIを使うイメージ

例えばのイメージですが、天気予報サイトの外部APIを使う場合の図です。
image.png
・中央のポータルサイトが例えばYahoo!のようなサイトで、ユーザーは住んでいる地域を登録しています。
・この地域情報を天気予報のAPIサーバーに渡して、その地域の天気の情報を検索して返します。
スマホアプリなども登録情報から同様に活用されます。

 

ざっくりと、こんな感じのイメージです。

ちなみにREST以外には..?

REST以外のものとしては、2000年代初頭までは標準化として「XML-RPC」や「SOAP」というものが使われていました。
しかし形式をXMLに限定していたり仕様が複雑だったりで、あまり広く普及するには至らなかったという背景があり、現在ではSOAPに代わり今回紹介したRESTが中心になっています。

 

なお「REST」は標準化ではなくあくまで設計思想なので、この点は留意しておきたいところです。

今回のまとめ

・RESTは「RESTの4原則に沿ったシンプルな設計思想」のことで、
  ①統一インターフェース
  ②アドレス可能性
  ③接続性
  ④ステートレス性
 の4つ。
・これらに沿ったシステムを「RESTfulなシステム」という。
・「REST API」と「RESTful API」は、呼び方の違いだけと思って良い。
・「REST」はRPCやSOAPと違って「標準化」ではないが、現代の主流の設計思想である。

 参考文献

イラスト図解式 この一冊で全部わかるWeb技術の基本

以上、最後までお読み頂きありがとうございました〜!

 

Wizではエンジニアとして一緒に働く仲間を絶賛募集しております。

ご興味のある方、是非ご覧下さい..!!

https://careers.012grp.co.jp/engineer

MySQLで行うパフォーマンスチューニング

こんにちは。バックエンドエンジニアの米山です。

私個人はJava+Oracleでお仕事をする期間が長かったのですが、昔はよくOracle側のパフォーマンスチューニングを行っていました。
弊社では、まだ採用ケースは少ないのですがDBにMySQLを採用し、経年によるデータ増加に対応する場合にどうするべきか?を考えてみようと思います。

準備


まずデータ増加した(ということにする)DBを用意します。
本当は100万件・・・と言いたいところですが、時間がかかりそうなので10万件程度にしておきます。

以下のような構成で作成しました。

  • MySQLのバージョンは8(8.0.22)
  • テーブルを3つ用意する(samples,cities,join_data)
  • samplesにデータを10万件insertしておく(カラム数や中のデータは適当に散らばせる)
  • samplesにINDEXを作成(SAMPLES_IDX_01、SAMPLES_IDX_02)

今回は、データはLaravelのSeederを使って作成しました。

チューニングしてみる


準備ができたら、実行計画を取ります。
MySQLで実行計画を取る場合は「EXPLAIN」句を使います。
f:id:t-yone3:20210514180331p:plain

データ量が大したことないのですぐ返却されましたが(笑)テーブルのJOINはB(cities)->A(samples)->C(join_data)の順に結合されていること、Cへの結合はHASH JOINで行われていることなどがわかります。
INDEXもほど良く使われていますね。

Oracleではお馴染みのオプティマイザヒントですが、MySQLでも使えるようです。
試しにJOINの順番を変えてみましょう。
JOIN順番を指示するには「JOIN_ORDER」ヒントを指定します。 f:id:t-yone3:20210514180509p:plain

実行計画が変わったことが確認できました。

実務でよくあるのがINDEXヒントの導入です。
ビッグデータ且つデータの分布率が思わしくなく、INDEXを作成したのに有効に効かない時にクエリで指定してあげる、というケースが多いです。

MySQLでのINDEXヒントはOracleとは違い、FROM句の中で行います。 f:id:t-yone3:20210514180905p:plain

どうでしょうか?実行計画が変わったのがわかりますか?
USE INDEX(SAMPLES_IDX_02)を指定することで、意図的にそちらのINDEXを使用するようになりました。
但し、INDEXヒントはデータの状況や選択したINDEXによっては、逆にパフォーマンスを悪化させることもありますのでINDEXの選定と併せて、使用はご注意下さい。

最後にSQL実行時間の測定です。
実行計画を見ながら、実際にSQLが早くなったのかを確認していきます。
mysqlコマンドに-vvvオプションをつけて実行します。
f:id:t-yone3:20210514181035p:plain

今回紹介した、JOIN_ORDERとUSE INDEXを両方適用してSELECT文を実行しました。
本当に僅かですが実行時間が変わったことと思います。

プログラムコードを弄るほどではないけど、画面表示のパフォーマンスを改善したい!という時には効果的です。

最後に


Wizではエンジニアを募集しております。

興味のある方、ぜひご覧下さい。

careers.012grp.co.jp

Next.js Serverless Functions APIを使ってみました

f:id:thunder_fury:20210512113531p:plain

はじめに

皆さんこんにちは、フロントエンドエンジニアのWooです。⚡️🌪

Next.jsの簡単なサーバー内の処理の実装ができたので、Vercelデプロイ基準でNext.jsのServerless Functionsの実装方法を調べてみました。

Serverless Functionsとは?

Serverlessを直訳すると、「サーバーがない」という意味ですが、実際はサーバーがない訳ではありません。

特定のタスクを実行するために、コンピュータを、あるいは仮想マシンにサーバーを設定していますが、実装者はサーバーの管理や運営をする必要がありません。つまり、サーバーリソースを意識することがなく、プログラムのみの実装に集中することができます。

VercelのServerless Functionsではユーザーの認証、フォームの送信、簡単なバックエンド処理からデータベースも実装でき、Vercelで完結することができます。

vercel.com

本来であればVercelのインストール/vercel.json作成/rootにAPIデータを作成するなど設定が少し必要ですが、Next.jsの良いところはprojectを作成したらpageフォルダの中にapiデータが自動生成されますのでjstsファイルを作成して処理を書いていくだけです。

環境構築

$ npx create-next-app serverless-test
$ cd serverless-test
$ npm run dev

ディレクトリー

pageフォルダ内にはapiフォルダが生成されhello.jsが作成されることが確認できました。

.
├── .next
├── page
│   ├── api
│   │    └── hello.js
│   ├── _app.js
│   └── index.js
└── ...省略...

hello.js中身はnameクエリを受信し、文字列を返すNode.jsの関数がデフォルトで含まれています。

export default (req, res) => {
  res.status(200).json({ name: 'John Doe' })
}

apiフォルダのjsファイルがそのままapiのurlになる仕様であり、http:localhost:3000/api/helloブラウザ接続でJSONが確認できました。

f:id:thunder_fury:20210511120413p:plain

TypeScriptで書きたい場合はimport type { NextApiRequest, NextApiResponse } from 'next' でTypeScriptが使えます。

import type { NextApiRequest, NextApiResponse } from 'next' // <-追加
export default (req: NextApiRequest, res: NextApiResponse) => {
  res.status(200).json({ name: 'John Doe' })
}

結果は同じです。

f:id:thunder_fury:20210511120413p:plain

GET & POST

各関数は、requestを受信し、responseを返す必要があります。 そうでなければ、タイムアウトしてしまいます。

データを返す最も簡単な方法としては、response.sendを使用します。このメソッドは、テキスト、オブジェクト、またはバッファを受け取ります。

export default (request, response) =>
  response.send({
    data: 'hello world',
  });

requeseは基本的に200を返しくれるのでPOSTとGET用のコードにstatusを変更する必要がありresponse.statusを使用してstatus codeを変更することができます。http-status-codes というパッケージを利用してもっと簡単に実装することができます。

$ npm i http-status-codes

GETの場合、get successの文字列を返してくれるサンプルです。

import Status from 'http-status-codes'

// GET -> http://localhost:3000/api/hello

export default (request, response) => {
  if (request.method !== 'GET') {
    return response.status(Status.BAD_REQUEST).send('')
  }
  return response.json({
    msg: `get success`,
  });
};

POSTの場合、post succesの文字列を返してくれるサンプルコードです。

import type { NextApiRequest, NextApiResponse } from 'next'
import Status from 'http-status-codes'

// POST -> `http://localhost:3000/api/hollo`
export default (req: NextApiRequest, res: NextApiResponse) => {
  if (req.method !== 'POST') {
    return res.status(Status.BAD_REQUEST).send('')
  }
  return response.json({
    msg: `post succes`,
  });
};

POSTではなく、直でapiにアクセスする場合は、400エラーになります。

f:id:thunder_fury:20210511120814p:plain

まとめ

以上、Next.jsでServerless Functionsを利用する使い方を調べてみました。

使った感想としてはdatabaseまで用意すると学習コストがかかりそうですが、簡単なメール送信機能が必要な小規模サイト(ランディングページ)はServerless FunctionsでPOSTができるのでNext.jsでスピーディーに実装するのも便利そうだと思いました。


最後になりますが、Wizではエンジニアを募集中です!

興味のある方は是非覗いてみてください!↓

careers.012grp.co.jp

第4回LT会を行いました。

第4回LT会レポ

今回のLT会の内容は

発表者: 3名

制限時間: 自由

テーマ: 自由

コメントツール:CommentScreen

で行いました。

それでは1つずつ発表を紹介していきます。

フロントエンドエンジニアがLaravelやってみて

f:id:wiz012:20210513123005p:plain
フロントエンドエンジニアがLaravelをやってみて
1人目の方には「フロントエンドエンジニアがLaravelやってみて」という内容を発表していただきました!

モチベーション

・CI/CD周りやアプリケーションの環境構築、構成などがバックエンドに任せきりだった

・技術的な障壁ができてしまう

やったこと

1. 基礎学習

 ・php, SQL, Dockerなどの基礎学習

2. やりたいアピールしてみる

 ・バックエンド課題に取り組む

 ・1on1などで上長に相談する

 ・アサインされているプロジェクトのバックの構成をみたり

3. 実装する

 ・家計の節約アプリをLaravelで構築してみる

 ・QR発行、通常の管理画面作成

得たこと

1. プロジェクトの構成考えやすい

 ・バック側、もしくはフロント側でプログラムを組んだ時の、

  実装スピードやパフォーマンスの違い

 ・データベースのリレーション構造への理解

2. フロントデータの受け渡し方について再度考えさせられた

 ・HTMLを圧縮しない方が良いこと

 ・bladeファイルのどの部分がバックエンドの変数に絡んでいるか

今後の課題

 ・pug-> html -> blade -> 配信の連携が難しい

 ・LaravelをAWSにデプロイする

前期成果発表

f:id:wiz012:20210423142638p:plain
ブランディング活動前期成果発表
2人目の方には「前期成果発表」について発表していただきました!

前期にやったこと

ブランディング施策

1. TechBlog運用

 ・前期(1~3月)のアンケート調査(全エンジニア向け)

  によると満足度はなり高かったです。

 ・「記事の投稿フローが難しかった」との意見をいただいたので、

  絶賛ドキュメント整理中です。

2. LT会

 ・同じく前期のアンケートをとったところ、半数以上の方から

  「やってよかった」というポシティブな意見をいただきました。

 ・その反面、「LT会の準備をする時間がない」という意見もいただきました。

エディタ作成

記事執筆にて文字を起こす時に使用するエディタ(WYSIWYGエディタ)のことです

1. 技術選定 ➡︎ Quill.js

2. ディレクターさん/ライターさんにヒアリング

今期はQuill.jsの学習とヒアリングをメインに行い、

来期からエディタ作成を行う予定です。

その他やったこと

1. コードレビュー会   tech.012grp.co.jp やったことがここに書かれています。是非ご覧ください!

2. アウトプット会

題材は自由で週に1回行いました。

3. Vue.js もくもく会

やっていることは、 backlogにある共有事項のストックアプリをチーム開発する(現在進行系)

WordPressメディア SSG化

f:id:wiz012:20210427113149p:plain
WordPressメディア SSG化
3人目の方には「WordPressメディア SSG化」について発表していただきました!

・コンテンツ増加によるサイトパフォーマンスの悪化

・WPデータ保持のためのステート管理の複雑化

 ➡︎これらの課題をSSGで解決できるのではと思ったことがきっかけだそうです。

f:id:wiz012:20210430111703p:plain
技術構成

f:id:wiz012:20210430112429p:plain
パーフォーマンス改善

f:id:wiz012:20210430112457p:plain
可読性改善

メリット

・パフォーマンス/可読性の向上

JavaScript統一による開発体験の向上 ➡︎ Lintによるエラー検出/コード整形が安易に

WordPressとViewが完全に分離される ➡︎ セキュリティリスクの低減 

デメリット

・コンテンツ反映までのタイムラグがある

・WP管理画面側の改修に手間がかかる

最後に~

第4回はこのような内容でした。

LT会の内容をもっと社外へ公開できるよう目指していきたいです。

また、

Wizではエンジニアを募集中です。

興味のある方は是非覗いてみてください!↓

careers.012grp.co.jp

 

【Laravel】DDDで、テストの時はDBを使わないリポジトリに差し替える

こんにちは。バックエンドエンジニアの河内です。

LaravelでDDDを採用しテストを書くさいに、DBを使わないリポジトリに差し替える方法について書きます。DDDについて詳細は省きますが、今回は以下のように処理が移るものとします。

Controller → UseCase → Repository

メリット・デメリット

なぜ、そんなことをするのでしょうか。メリットとしては、以下が考えられます。

メリット

  1. DB設計〜マイグレーションの工程を後に回せる
  2. テスト時のコスト(負荷、時間)を低減できる

一方で以下のようなデメリットと言えるようなものもあるかと思います。

デメリット

  1. そのリポジトリを作る必要がある
  2. 結局、DB使うテスト結果も気になる

実例

というわけで、簡単な実例で考えていきます。 なんらかのアイテムを登録するユースケースです。

プロダクトコード

コントローラ

コントローラです。説明にフォーカスするため、いろいろ削ぎ落としています。PHPDocもありません。アクションでレスポンスしてません。 フォームリクエストでは、受け取った値をユースケースに渡すため、getDataなるメソッドで処理が適切におこなわれたものとします。

<?php

namespace App\Http\Controllers;

use App\Http\Requests\CreateItemRequest;
use App\Packages\AwesomeSystem\UseCase\CreateItem;

class ItemController extends Controller
{
    public function __construct(CreateItem $createItem)
    {
        $this->createItem = $createItem;
    }

    public function store(CreateItemRequest $request)
    {
        $this->createItem->handle($request->getData());
    }
}

ユースケース

ユースケースです。コンストラクタ引数のリポジトリがインターフェースです。handleメソッドで、受け取ったItemのエンティティをcreateメソッドへ渡しています。

<?php

namespace App\Packages\AwesomeSystem\UseCase;

use App\Packages\AwesomeSystem\Domain\Item;
use App\Packages\AwesomeSystem\Infrastructure\ItemRepositoryInterface;

class CreateItem
{
    protected $itemRepository;

    public function __construct(ItemRepositoryInterface $itemRepository)
    {
        $this->itemRepository = $itemRepository;
    }

    public function handle(Item $item)
    {
        $this->itemRepository->create($item);
    }
}

(DBを使う)リポジトリ

DBを使うリポジトリです。Eloquentに依存しているので、ファイル名をEloquentで始めています。依存しているので、いきなり呼び出してcreateメソッドで保存処理を実行してしまいます。今回は、アイテムの名前だけ登録するものとします。エンティティItemと名前が重複するので今回はモデルのほうをEloquentItemとしました。

<?php

namespace App\Packages\AwesomeSystem\Infrastructure;

use App\Models\Item as EloquentItem;
use App\Packages\AwesomeSystem\Domain\Item;

class EloquentItemRepository implements ItemRepositoryInterface
{
    public function create(Item $item)
    {
        EloquentItem::create(['name' => $item->getName()]);
    }

    public function first(): Item
    {
        return EloquentItem::first();
    }
}

リポジトリインターフェース

最低限、リポジトリに実装されるべきメソッドが羅列されています。

<?php

namespace App\Packages\AwesomeSystem\Infrastructure;

use App\Packages\AwesomeSystem\Domain\Item;

interface ItemRepositoryInterface
{
    public function create(Item $item);
    public function first(): Item;
}

サービスプロバイダ

ユースケースのコンストラクタ内で、ItemRepositoryInterfaceを指定したら、(プロダクトでは)EloquentItemRepositoryを呼び出してほしい。これは明示的にそう言ってあげないとLaravelは知るよしもありません。サービスプロバイダで結合します。

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Validator\CustomValidator;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->bind(
            \App\Packages\AwesomeSystem\Infrastructure\ItemRepositoryInterface::class,
            \App\Packages\AwesomeSystem\Infrastructure\EloquentItemRepository::class
        );
    }

エンティティ

ちなみに、Itemエンティティは以下のようなイメージです。$nameは値オブジェクトでもよさそうですがこれも今回は省きます。

<?php

namespace App\Packages\AwesomeSystem\Domain;

class Item
{
    protected $name;

    public function __construct(string $name)
    {
        $this->name = $name;
    }

    public function getName(): string
    {
        return $this->name;
    }
}

テストコード

ユースケーステスト

さて、このユースケースのテストを書いてみます。タイトルのとおり、テストはDBを使わないリポジトリでおこないます。

<?php

namespace Tests\Unit\Awesome\UseCase;

use App\Packages\AwesomeSystem\Domain\Item;
use App\Packages\AwesomeSystem\Infrastructure\InMemoryItemRepository;
use App\Packages\AwesomeSystem\UseCase\CreateItem;
use Tests\TestCase;

class CreateItemTest extends TestCase
{
    public function testアイテム新規登録()
    {
        // ①
        $itemRepository = app()->make(InMemoryItemRepository::class);
        // ②
        app()->bind(CreateItem::class, function () use ($itemRepository) {
            return new CreateItem($itemRepository);
        });

        $name = 'アイテムテスト';
        // ③
        (app()->make(CreateItem::class))->handle(new Item($name));
        // ④
        $item = $itemRepository->first();

        // ⑤
        $this->assertEquals($name, $item->getName());
    }
}

①で、DBを使わないリポジトリを生成しています。②が、先述のサービスプロバイダでの結合と異なり、DBを使わない①との結合となっています。③でその結合でもってユースケースを生成し、アイテムエンティティを渡して保存しました。④でデータを1つ取り出します。初投入されたことが明らかなので、firstメソッドで取り出しました。⑤で、投入データと保存データが合致するか確かめます。これで、DBを使わずテストをおこなえました。

(DBを使わない)リポジトリ

インメモリリポジトリについて触れておきます。内部では、プロパティ$dataを、データストアと見立て、createメソッドでは受け取ったアイテムエンティティをpushしているだけです。firstメソッドではコレクションの最初のエンティティを返すだけです。

<?php

namespace App\Packages\AwesomeSystem\Infrastructure;

use App\Packages\AwesomeSystem\Domain\Item;

class InMemoryItemRepository implements ItemRepositoryInterface
{
    private $data;

    public function __construct()
    {
        $this->data = collect();
    }

    public function create(Item $item)
    {
        $this->data->push($item);
    }

    public function first(): Item
    {
        return $this->data->first();
    }
}

実例からメリット・デメリットを考察する

あらためて、メリットをふり返ります。

メリット

1. DB設計〜マイグレーションの工程を後に回せる

実例ではプロダクトコードから紹介しましたが、テストを書くのであればTDD…そのTDDの教えるところはテストファーストです。

だとすると、以下の順番でコードを書いていくことになろうかと思います。

  1. ユースケーステスト
  2. ユースケース
  3. リポジトリインターフェース
  4. (DBを使わない)リポジトリ

これだけあればユースケースのテストがかないます。これまでDBについて考えることはありませんでした。したがって、マイグレーションも登場しません。

ただし、エンティティ作成にあたってはドメインモデルを深く考える必要があります。それはDDDで本質的な作業です。あくまで、DBについての関心はひとまず置いておける、ということです。

2. テスト時のコスト(負荷、時間)を低減できる

DBテストでは、テーブルをTRUNCATE〜INSERTを繰り返す必要があり、テストケースが増えるとそのコストが無視できません。インメモリリポジトリではDBにまつわる問題をわきに置いて、ロジックのテストに集中できていることになります。

デメリット

1. そのリポジトリを作る必要がある

当然ですが、DBを使うリポジトリと別にDBを使わないインメモリリポジトリを作る必要があります。なければないでプロダクトコードを実装することもできます。テストケースの数によっては、インメモリリポジトリを作らないで済ませることもできそうですし、ケースバイケースです。

ただ、インターフェースを用意しインメモリリポジトリを作ってしまえば体感としては9割方完成はしていて、後はDBを使うリポジトリを用意しあてがうだけ…という感じです。各リポジトリを同じ工数かけて2つ作らねば…という感じではないのかな、と思います。

2. 結局、DB使うテスト結果も気になる

デメリットといいますか、プロダクトコードはDBを使うリポジトリで動くので、DB込でアプリケーションが本当に動くかの統合テストは別途必要になるかと思います。

まとめ

リポジトリの呼び出しのさいインターフェースに依存することで、プロダクトコードとテストコードで用いるリポジトリを使い分けることができました。インメモリリポジトリを使うことで、インフラ(=DB)の事情にとらわれず、ロジックのテストに集中できるかと思います。

最後に

Wizではエンジニアを募集しております。 興味のある方、ぜひご覧下さい。

careers.012grp.co.jp