NestJSに入門してみる(DI編)

前回の記事でNestJSに入門しました。

今回はその続きです。タイトルにある通りDIについて書いていきます。

DIとは

その前に軽くDIについておさらいしておきます。

DIはDependency Injectionの略で日本語に訳すと依存性の注入です。

例を挙げると、

class Hoge {
  function hoge() {
    console.log('hogehoge');
  }
}

class Fuga {
  function fuga() {
    const hoge = new Hoge();
    hoge.hoge();
  }
}

const fuga = new Fuga();
fuga.fuga();

上記の例の場合、Fugaクラスのfuga()という関数はHogeクラスを内部でインスタンス化しているため、Hogeクラスに深く依存してしまっています。

こうなると密結合でテストも書きづらいです。

そもそもHogeクラスのインスタンスを生成する責務はFugaクラスには本来無いはずです。

なので、依存するクラスを外部から渡してあげる。これが依存性の注入です。

方法としてはコンストラクタインジェクションやセッターインジェクション等があります。

// コンストラクタインジェクション
class Hoge {
  hoge() {
    console.log('hogehoge');
  }
}

class Fuga {
  constructor(hoge) {
    this.hoge = hoge;
  }

  fuga() {
    this.hoge.hoge();
  }
}

const fuga = new Fuga(new Hoge());
fuga.fuga();
// セッターインジェクション
class Hoge {
  hoge() {
    console.log('hogehoge');
  }
}

class Fuga {
  setHoge(hoge) {
    this.hoge = hoge;
  }

  fuga() {
    this.hoge.hoge();
  }
}

const fuga = new Fuga();
fuga.setHoge(new Hoge());
fuga.fuga();

個人的にはコンストラクタインジェクションの方が良いと思います。

セッターインジェクションの場合、Fugaクラスのインスタンスを生成した時点ではhogeプロパティには何も入っておらずsetHogeが呼ばれないとエラーになるためです。

インスタンス化した時点で必要なものがすべて入った状態を作る(=完全コンストラクタ)のが設計としても良いと思います。

NestJSでのDI

NestJSでのDIは前回の記事で実は少し触れています。

app.service.tsは以下のようになっています。

import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }
}

@Injectable()というデコレータが付いており、これによって依存関係として注入できるクラスであることを示します。

一方、app.module.tsはこちら。

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

providersで先程のAppServiceを指定しています。

これによって、AppModuleの中ではAppServiceを注入することができます。

ちなみに省略せずに書くとこのようになります。

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [
    {
      provide: AppService,
      useClass: AppService
    }
  ],
})
export class AppModule {}

useClassで実際に呼び出されるクラスを指定し、provideで何という名前で扱うかを指定します。

抽象に依存したい

しかしこのままでは具象クラスを直接注入することになります。

SOLID原則のDに当たる依存性逆転の法則では「抽象に依存すべきである」とされています。

app.controller.tsを見ると、

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller('cats')
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

AppControllerクラスはAppServiceクラスに依存しています。

AppServiceクラスではなく他のクラスを使うことになった場合、AppControllerクラス自体も修正が必要です。

そうならないためにinterfaceや抽象クラスを用意してAppServiceクラスはそれを実装または継承する形にし、AppControllerクラスはその抽象に依存させる形が望ましいです。

NestJSでのやり方を調べると公式に以下の項目が書いてありました。

Non-class-based provider tokens

これを読むと、以下2点で実現できそうです。

  • AppModuleprovidetokenと呼ばれる文字列を指定する
  • 依存注入するクラスで@inject()デコレータを使い、↑のtokenを指定する

というわけでやってみます。以下のinterfaceを作りました。

export interface AppServiceInterface {
  getHello(): string;
}

そして具象クラスも上記のinterfaceを実装する形に修正します。

import { Injectable } from '@nestjs/common';
import { AppServiceInterface } from './app.service.interface';

@Injectable()
export class AppService implements AppServiceInterface {
  getHello(): string {
    return 'Hello World!';
  }
}

AppModuleも以下の通り修正。

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [
    {
      provide: 'SERVICE',
      useClass: AppService
    }
  ],
})
export class AppModule {}

そして最後にAppController@inject()を使います。

import { Controller, Get, Inject } from '@nestjs/common';
import { AppServiceInterface } from './app.service.interface';

@Controller('cats')
export class AppController {
  constructor(@Inject('SERVICE') private readonly appService: AppServiceInterface) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

修正後のコードでも無事Hello Worldできました。

実際には公式の例にあるようにtokenを定数ファイルで管理する方法が良さそうですね。

まとめ

以上、NestJSでのDIについて書きました。

普段LaravelではServiceProviderを使っているのでNestJSでも同じことができるのは嬉しいです。

そして改めてTypeScriptの恩恵も感じました。TypeScript万歳。

NestJSに入門してみる

タイトル通り、NestJSに入門してみます。

最近Nuxtのアップデート作業も落ち着いてきて次はTypeScript導入、というタイミングでバックエンドもTypeScriptで書けたら面白いかもと思ったのがきっかけです。

NestJSとは

A progressive Node.js framework for building efficient, reliable and scalable server-side applications.

とあるように効率的かつ信頼性の高いスケーラブルなサーバーサイドアプリケーション構築のためのNode.jsフレームワークです(直訳)。

Node.jsのフレームワークといえばExpressが長らくその地位を占めていたように思いますが、最近ではNestJSの名前も以前より聞くようになってきました。

GithubのStartも5万7千件、対するExpressは6万1千件なのでだいぶ近づいています。

と思ったら内部的にExpressを使っているみたいです。

公式ドキュメントはまだ日本語対応していないようです。

はじめてみる

というわけで何はともあれ始めてみます。

公式ドキュメントに沿って進めます。

まずはNest CLIをインストールします。

$ npm i -g @nestjs/cli

余談ですが-gでグローバルインストールしようとしたらこんな警告が出ました。

npm WARN config global `--global`, `--local` are deprecated. Use `--location=global` instead.

いつの間にかdeprecatedになっていたようです。

とりあえずインストールできたか確認してみます。

$ nest -v
zsh: command not found: nest

いきなりつまずきました。

色々調べた結果、.zshrcにaliasを設定する必要がありました。

npm root --location=global

でパスを確認して以下を追記します。

alias nest="{上記で確認したパス}/@nestjs/cli/bin/nest.js"

追記したら

$ source ~/.zshrc

で反映させると、

$ nest -v
9.5.0

無事表示されました。

ではNestJSのプロジェクトを作成します。

$ nest new nestjs_sample

パッケージマネージャーをnpm / yarn / pnpmの中から選んでしばらく待つとセットアップが完了します。

$ nest start

を実行するとポート3000番で起動します。

とりあえずHello Worldできました。

NestJSの構成

自動で生成されたファイルを見てみます。

srcディレクトリの下に生成されたファイルがアプリケーションコードです。

NestJSはController / Provider / Moduleの3つが主な要素となります。

Controller

ControllerMVCにおけるControllerと同じくリクエストを受けてレスポンスを返す処理を担当します。

加えてNestJSではルーティングもControllerで設定します。app.controller.tsを見てみましょう。

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

@Controller()というデコレータでControllerを定義しています。

@Controller()デコレータはprefixを引数に取ることができ、それによってルートをグルーピングできます。

例えば@Controller('cats')とするとlocalhost:3000/catsに対応します。

同様に@Get()デコレータもパスパラメータを引数に取ることができます。

ID等、動的なパラメータを取りたいときは@Get(':id')のようにします。

また、お察しの通り@Get()以外に@Post()@Put()はそれぞれPOST / PUTに対応しています。

Provider

続いてapp.service.tsを見てみます。

import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }
}

Providerはサービスやリポジトリ、ファクトリ等、他のクラスに依存関係として注入されるものを指します。

LaravelでいうところのServiceProviderで管理されるクラスに相当する、という認識です。

@Injectable()という新しいデコレータが登場しました。

これはDIコンテナで管理できる(=依存関係として注入できる)クラスであることを示すためのメタデータを付加します。

Module

最後にapp.module.tsを見てみます。

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Controllerと同様に@Module()デコレータでModuleであることを宣言します。

Each application has at least one module, a root module. The root module is the starting point Nest uses to build the application graph - the internal data structure Nest uses to resolve module and provider relationships and dependencies. While very small applications may theoretically have just the root module, this is not the typical case. We want to emphasize that modules are strongly recommended as an effective way to organize your components. Thus, for most applications, the resulting architecture will employ multiple modules, each encapsulating a closely related set of capabilities.

とあるように、Moduleという名前の通り各機能ごとに必要なControllerやService、その他のクラスをまとめ、カプセル化されたModule(公式ドキュメントではFeature modulesと呼んでいます)をアプリケーションのRoot Module(上記のapp.module.tsがそれですね)でimportするのが基本形です。

公式ドキュメントからコードを引用すると以下のようなイメージです。

// /src/cats/cats.module.ts
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class CatsModule {}
// /src/app.module.ts
import { Module } from '@nestjs/common';
import { CatsModule } from './cats/cats.module';

@Module({
  imports: [CatsModule],
})
export class AppModule {}

main.ts

以上Controller / Provider / Moduleを説明してきましたが、アプリケーションのエントリーポイントとなるのがこのmain.tsです。

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();

NestFactory.create(AppModule)でNestJSのインスタンスを生成し、ポート3000番でlistenしています。

まとめ

以上、NestJSの基本をざっくりまとめてみました。

次回以降、DIやORM等について少しずつ進めていければと思います。

『フロントエンド開発のためのテスト入門』

先日こちらの本を買いました。

https://www.amazon.co.jp/%E3%83%95%E3%83%AD%E3%83%B3%E3%83%88%E3%82%A8%E3%83%B3%E3%83%89%E9%96%8B%E7%99%BA%E3%81%AE%E3%81%9F%E3%82%81%E3%81%AE%E3%83%86%E3%82%B9%E3%83%88%E5%85%A5%E9%96%80-%E4%BB%8A%E3%81%8B%E3%82%89%E3%81%A7%E3%82%82%E7%9F%A5%E3%81%A3%E3%81%A6%E3%81%8A%E3%81%8D%E3%81%9F%E3%81%84%E8%87%AA%E5%8B%95%E3%83%86%E3%82%B9%E3%83%88%E6%88%A6%E7%95%A5%E3%81%AE%E5%BF%85%E9%A0%88%E7%9F%A5%E8%AD%98-%E5%90%89%E4%BA%95-%E5%81%A5%E6%96%87/dp/4798178187

以前の記事でもフロントエンドのテストについて触れたので改めて勉強してみようと思った次第です。

表紙にあるように、「何から始めるべきか」を示してくれる1冊でした。

テストを書く目的

第1章ではテストを書く目的と障壁について書かれています。

テストを書く目的として挙げられていたのは以下の通りです。

  • 事業の信頼のため
  • 健全なコードを維持するため
  • 実装品質に自信を持つため
  • 円滑なコラボレーションのため
  • リグレッションを防ぐため

普段バックエンドの実装を行うときはクラスごとにユニットテストを書いていますが、「健全なコードを維持するため」と「実装品質に自信を持つため」のところが大きいように思います。

例えばリファクタリングを行う際、既存の動作が壊れていないことが大前提となります。

それをどう担保するかを考える上で必要なのがテストです。

テストがしっかり書かれていればリファクタリングを行った際にも動作を担保することができます。

また、小さい単位で実装と併せてテストを書くことで各クラスの責務を意識しながら実装することにも繋がりますし、動作が担保されたクラスを使うことでテストで確認したい責務も自ずと明確になります。

小さい単位で、というのは例えば値オブジェクトです。

値オブジェクトのテストがしっかり書けていればその動作は担保されているため、それを用いたエンティティの責務や振る舞いも明確になります。

同様にエンティティのテストを書くことでリポジトリ等の動作も...というように小さな単位から積み上げていくことでより強固な実装になります。

そうすると自信を持って実装することができます。

なので先程述べた2つが大事であると考えていました。

それ以外のところで言うと「円滑なコラボレーションのため」もなるほどと思いました。

テストコードは、単純なテキストドキュメントよりも優れた補足情報です(図1-4)。テストには1つずつタイトルが与えられ、どのような機能を提供しているのか、どのような振る舞いを持つのかが記されています。それらのテストをパスしていますから、補足内容と実装内容が異なるということもありません。

吉井健史著『フロントエンド開発のためのテスト入門』 p.8

↑の文章は今所属しているチームで共有している「テストを仕様書にする」という文化とまさに同じです。

テストコードを見ればそのテスト対象の仕様がわかる状態が理想であり、またテキストによる仕様書と異なって仕様変更があった際にはテストが失敗するので腐ることもありません。

またレビューの際にもテストコードを見れば要件を満たしているかどうかのチェックもできるため、要件が不足したままリリース、ということもなくなります。

CIで自動テストを行えばそもそもレビュー前にテストが落ちていたら修正が必要なのでレビュワーが指摘する手間も省けます。

こうした目的を果たすために、テストを書く習慣をチームに根付かせ、常にメンテナンスしていくことが求められます。

テストを書く障壁

一方で、テストを書く文化や習慣が無い状態だとそもそもテストを書くことに対してネガティブだったりします。

  • テストを書く時間があればその分機能実装に時間を使いたい

とか、

  • そもそもテストをどう書けばいいかわからない

とか。

まず前者については長期的に見ればテストを書いた方が工数の削減に繋がります。

なぜかと言うと問題を早期に潰せるからです。

機能の実装だけ行う場合と機能の実装に加えてテストを書いた場合、そこだけを見れば確かに前者の方が工数は短くなるでしょう。

ただ、例えばQAチームによる手動テストの際にバグが発見された場合は手戻りが発生します。

その際にかかる工数はバグの原因の調査、修正、再度QAチームによる検証を勘案するとトータルではテストを書く時間より長くなります。

QAチームが見つけてくれた場合はまだ良い方で、万が一見逃してしまってそのままリリースされた場合、しかもバグを埋め込んだまま要件追加に対応してより複雑さが増していた場合はもっと大変です。

バグを見つけるのは早期であればあるほど原因の特定も修正も容易です。

予めテストを書いておくことはこうした惨事を防ぐことに繋がります。

後者の「そもそもテストをどう書けばいいかわからない」については既存のテストコードを見て、どのような観点でテストしているかを参考にすることが第一歩かと思います。

もしテストコードが全くない場合はまさに本書がその一助となるはずです。

テストの種類

ここまではフロントエンドに限らずバックエンドにも当てはまる話でしたが、第2章からはいよいよフロントエンドの話に入っていきます。

フロントエンドのテストの種類は以前の記事に書いたように以下の4つに大別されます。

これらの分類に加え、テストタイプによる分類もあります。

機能テスト

フロントエンド開発はコンポーネントベースでの開発が基本です。

機能テスト(インタラクションテスト)は開発対象のコンポーネントの機能に不具合が無いかを検証するテストです。

実際のブラウザをヘッドレスモードで起動し、UIオートメーションを用いてテストする以外にも仮想ブラウザ環境でテストする方法もあります。

フロントエンド開発のテストはこの機能テストが中心となります。

非機能テスト

名前の通り、機能に関すること以外のテストを指します。

フロントエンド開発ではアクセシビリティテスト等がこれに当たります。

正直に言いますと、本書を読むまでフロントエンドのテストは上記の機能テストだけだと思っていました。

しかし現在では何らかの制約がある場合でも可能な限り多くの人がWebサイトを使えるよう、WAI-ARIA等のアクセシビリティに関する仕様が定められています。

こうした観点からのテストもフロントエンドにおいては必要です。

リグレッションテスト

差分を検出して想定外の不具合が出ていないかを検証するテストです。

フロントエンドは特にUIに関わるのでビジュアルリグレッションテストが重要視されます。

テスト戦略

2章の最後にテスト戦略についていくつかのケースを挙げて書かれています。

テストがなく、リファクタリングに不安がある場合

リリース済みの機能をリストアップし、リファクタリング前後で欠陥が混入していないことを検証するリグレッションテストを行います。

リファクタリング前にテストを書くことでリファクタリングに安心して取り組めるようになります。

また、APIサーバーへの依存がきれいに分割されていない場合はモックサーバーを使用した結合テストが効果的です。

レスポンシブレイアウトを含むプロジェクトの場合

Storybookを用いたビジュアルリグレッションテストを中心とします。

PC向けに設定したスタイルがスマートフォン向けのスタイルにも適用されてしまうことを未然に防ぐことができます。

データ永続層を含めたE2Eテストを行いたい場合

実際のAPIサーバーを含めたE2Eテストを行いたい場合はステージング環境を用意した上でブラウザ上でUIオートメーションを用いたテストを行います。

それ以外の方法としては関連システムを再現するテストコンテナを用意する手法もあります。

上記それぞれ具体的な内容については本書をご参照ください。

まとめ

以上冒頭2章をまとめてみました。

第3章からは具体的なテストの書き方に入っていくのでそちらについてもまたそのうち書くかもしれません。

コーヒーのすすめ

今年のGWは皆様いかがお過ごしでしたでしょうか。

僕は例年よりも規則正しい生活を送ることができて明日からの仕事にも割とスムーズに戻れそうです。

とはいえ休みは終わってほしくないです。

今回は久しぶりにプライベートな記事です。

ハンドドリップコーヒー

皆さんコーヒーはお好きですか?

僕も好きで仕事中も一息入れたいときに淹れて飲んでます。

ずっと缶コーヒーとかインスタントで飲んでいたんですが、去年の12月頃から自分で豆を挽いて淹れるようになりました。

自分で豆を挽くと豆の違いや挽き方の違いで味がガラッと変わって面白いです。

コーヒー豆は大別するとストレートとブレンドに分けられます。

1つの生産国の豆に絞ったものがストレートです。キリマンジャロとかモカとかがそれです。

一方複数の生産国の豆が名前の通りブレンドされたものがブレンドです。

ブレンドはその店の個性が出るので面白いですね。

シングルの中でもさらに細かく1つの農園・1つの品種に絞ったものをシングルオリジンと言います。

より豆の個性が際立ったコーヒーになります。

焙煎

焙煎は深煎り・中煎り・浅煎り等があります。

深煎りの方が見た目にも色が濃く、香ばしい香りと苦味が前面に出たコーヒーになります。

一方浅煎りは酸味が引き出された味わいになります。中煎りはその中間でバランスの取れた味。

いわゆるサードウェーブコーヒーは浅煎りの豆が使われています。

個人的にも浅煎りの方が好きです。

挽き方

豆の挽き方は5種類あります。

mystyle.ucc.co.jp

一番細かい極細挽きはエスプレッソを淹れるときの挽き方で、ペーパードリップで淹れる場合は中細挽き以上かと思います。

細かく挽くほど粉とお湯の触れる面積が大きくなるため、苦味が強く出ます。

粗挽きになるほど酸味を感じられるコーヒーになります。

自宅のコーヒーミルはやや粗めに挽くように設定してあります。

とはいえ、自分で挽くと全然粒度が一定にならないですね。。

おすすめのお店

個人的に好きなお店も紹介します。

かみかわ珈琲焙煎所

kamikawacoffee.com

1軒目は豊中にあるかみかわ珈琲焙煎所。阪急宝塚線曽根駅の近くにあるお店です。

「かみかわ珈琲焙煎所はお砂糖やミルクがいらないコーヒーを焙煎しています。」とある通り、何も入れなくても本当に美味しいです。

ブラジルの浅煎りが好きでリピートさせてもらってます。

SOUL DRIP COFFEE

souldripcoffee.com

2軒目はSOUL DRIP COFFEE。

友達のお店で僕がコーヒーを始めるきっかけになったお店です。

以前は神戸の中突堤中央ビルで営業していましたが、ビルの取り壊しにより惜しくも閉店。

現在はオンラインショップの他、神戸の色んなイベントに出店されています。

スペシャルティコーヒーを飲んだことが無い方はぜひ一度飲んでみてください。

手軽に試すなら水出しアイスコーヒーがオススメです。

まとめ

僕もまだまだビギナーでわからないことだらけですがコーヒーは面白い趣味だと思います。

これを読んで興味が湧いた方はぜひ始めてみてください。

フロントエンドのテスト

先日チーム内でフロントエンドのテストについての話になったので軽くまとめておきたいと思います。

テストの単位

フロントエンドのテストは大きく以下の4つに分けられると思います。

静的解析

ESLintによる静的解析を始め、TypeScriptにおける型チェック等も含みます。

個人的にはここは最低限必要なところかと思います。TypeScriptにおいてはそもそもコンパイルできない訳ですし。

ESLintも単純な構文エラーやチーム内でのコーディング規約から外れたものを検知できるので最低限担保すべきコードの質を維持する上で絶対かと思います。

単体テスト

ReactにしてもVueにしてもコンポーネント駆動開発が基本です。

バックエンドだとクラスごとに単体テストを書くようにコンポーネントごとの単体テストが必要になってくると思います。

今回の記事はこの単体テストを中心に書いていきます。

結合テスト

複数のコンポーネントやクラスを組み合わせて正しく動作するかを確認するテストです。

またReactだとHooks等の副作用も対象に含みます。

E2Eテスト

実際にブラウザ上で各シナリオ(テストケース)ごとに期待された挙動になるかどうかを確認するテストです。

テストツールとしてはCypressPlaywrightSelenium等があります。

以上4種類のテストについてはTesting Trophyが有名です。

引用元: https://kentcdodds.com/blog/the-testing-trophy-and-testing-classifications

最上位のレイヤーにE2Eテスト、最下層が静的解析となっています。

上のレイヤーに行くほど、

  • フィードバックは遅く
  • 実装コストは高く
  • 信頼性は向上する

という傾向があります。

これは以前エンジニア文化祭の記事で少し触れたテストピラミッドと同様です。

E2Eテストは信頼性が高いものの実行にも時間がかかり、また修正も容易ではありません。

一方で単体テストは修正や実行時間は短くて済みますが、アプリケーション全体のテストではないので信頼性という観点ではE2Eテストに劣ります。

つまりレイヤーごとにメリット・デメリットを比較し、どのレイヤーのテストに力を入れるのがチームやプロダクトにとってコスパが良いのか、戦略を立てる必要があります。

単体テスト

個人的には単体テストを厚くしていくことが基本だと考えています。

理由としては、

  • 実装コストが低いこと
  • 現代のフロントエンドはコンポーネント駆動であること

が挙げられます。

特に2点目はできるだけ小さい粒度でのテストを積み上げていくことがシステム全体の信頼度を上げることに繋がると考えているためです。

先程書いた通りコンポーネントが最小の単位なのでそこの単体テストは必要になるだろうということです。

では具体的にどうテストしていくのかですが、現状の考えとしては以下のとおりです。

  • コンポーネントの見た目やDOM操作等の関数: Storybook
  • DOM操作以外のロジックを伴う関数: Jest / Vitest

基本的にはテストツールはJest (Viteを使っている場合はVitest)で良いと思います。

ただコマンドライン上で動くので例えばwindowを使う関数などはそのままでは使えません(モックする方法はあるみたいですが)。

また、例えばVueのrefを使ってDOMを操作する関数もJest等ではテストしづらいです。

なのでそうしたものについてはStorybookを使ってコンポーネントの見た目と併せてテストするのが良いのではと思います。

逆に言えばそれ以外の関数はすべてJest / Vitestでテストを書く方針で良いと思います。

特にNuxt3ではComposablesによって関数を切り出すことができるようになったのでよりテストしやすくなりました。

DOM操作以外のロジックを伴う関数に関しては/composables直下にドメインごとに切り出してテストを書いていく、という方向で考えています。

まとめ

以上、フロントエンドのテストについてまとめてみました。

まだフロントエンドのテスト戦略についてはベストプラクティスがわからない状態なので試行錯誤しながら進められたらと思います。

その前にNuxt3へのアップデートをしっかり完了させたいと思います。

ブログで1年間アウトプットしてみた結果

タイトル通り、このブログを書き始めてから1年が経過しました。

スマレジでは週1回(1年経過後は2週に1回)ブログで発信することでもらえるブログ手当という制度があるのですが、当初は完全にそれ目的でした。

が、良い副作用が結構あったと感じているので今日はその辺を書こうと思います。

インプットの質を意識するようになった

記事を書くというアウトプットのためには何かしらのインプットが必要です。

それは例えば業務中に得た知見であったり、個人開発であったり、あるいは技術書だったりと様々です。

これら全てに言えることですが、ただ漫然と行っているだけでは質の高いインプットにはなりません。

例えば業務中に何か詰まったところがあったとします。

単に解決策が欲しいだけならググって上位に表示されたページに書いてあるコードをコピペすればOKな場合がほとんどです。

が、

  • そもそもの原因は何か
  • その解決策はなぜ解決できるのか
  • 対症療法的な方法で限られたケースでだけ通用する方法ではないか

等を理解できないまま目先の問題を解決するだけだと何も身につかないでしょう。

恐らく同じような問題がずっと付きまとうはずです。

なのでこれらを理解できるようなインプットを意識するようになりました。

具体的には、

等です。

フレームワークを使っていると便利なヘルパーが多く、それに頼ってしまいがちですが、そもそも内部ではどういうことを行っているのかを理解した上で使うようにするとより解像度が上がります。

また、インプットの質を高めることは即ちアウトプットの質も高めることに繋がります。

より解像度の高いインプットは言語化しやすいからです。

逆に言うと言語化しづらかったりあやふやだったりする場合はインプットが不十分または解像度が低いと考えられます。

よく「一番の学習方法は人に教えることだ」みたいなことを言われますが、人にわかりやすく説明できるレベルまで落とし込めていればかなり深くまで理解できているのではないかと思います。

自分の興味関心や不足している部分が見える

これは記事を書いていてとても感じるところです。

もちろん全方位に対して知識が不足していることは痛感しているのですが、その中でもその時の自分の興味が強い分野の記事が多くなる傾向があります。

最近だと完全に興味がフロントエンド寄りですね。

あらゆる分野をまんべんなく網羅できれば理想ですが、どうしても偏りは生じてしまいます。

そうした傾向は無意識のうちに生じるので、記事一覧を見返した時に初めて気づきます。

で、自覚した後に

  • 今興味のある分野をもっと尖らせていくか
  • それともそれ以外の分野を勉強していくか

を選択することが後々のキャリア等にも繋がってくるのではとぼんやり考えています。

エンジニアとして自分の強みと言える分野・尖った部分を持つということも大事ですし、

まだまだ経験が浅いので持っておくべき・実践すべき知識というのも沢山あります。

そうした意味で継続的にアウトプットすることで自分の現状が可視化できるというのは当初考えていなかったメリットでした。

現状は先に書いた通り記事の内容はフロントエンド寄りですが、今のチームではフロントエンド専任というわけではなくバックエンドも担当しています。

なのでバックエンドに関する記事も書いて実践していかないとなと感じる一方、このままフロントエンドに特化するのも面白そうだなと考えています。

これについてはぼちぼち考えていきたいと思います。

アウトプットする場があるから色々試せる

例えば今のチームで言うと業務で必要な知識は

辺りです(細かいところで言うともっとあると思いますが)。

ですが、業務で使わない技術についても色々勉強してみたいことはあります。

そんな時にブログがあると単純に記事のネタになるので気軽に試せるのが割と心理的に良かったように思います。

個人開発で細々とやるだけなので別にブログが無くてもOKなんですが、ここで先程の話ともリンクします。

記事にするためには小手先ではなくしっかりと整理して言語化し、文章にまとめる必要があります。

業務で使わない技術についても人に教えられるぐらい理解して使いこなせるようになるには質の高いインプットが必要です。

つまり、ブログというアウトプットの場があることで色んな技術に触れる機会が生まれ、また学びも深くなり、エンジニアとしての地力を上げることに繋がっているということです。

なので今後も業務中に得た知見以外にも色々一人で試してみた結果を記事にできればと思います。

Nuxt3&Vue3アップデートで地味に詰まったところ

最近は細々したタスクを担当しつつ、もっぱらNuxtのアップデート作業に従事しています。

で、色々やっているうちにちょこちょこ知見も溜まってきたのでそれらをまとめてみようと思います。

変数の値

Vueのcomputedよく使うと思います。

例えばVue2までのOptions APIで書くとこんな感じ。

<script>
export default {
  name: 'Hoge',
  data() {
    return {
      hoge: 1
    }
  },
  computed: {
    fuga() {
      return this.hoge * 2;
    },
    piyo() {
      return this.fuga + 1;
    }
  },
  methods: {
    add() {
      this.hoge++;
    }
  }
}
</script>

<template>
  <p>hoge: {{ hoge }}</p>
  <p>fuga: {{ fuga }}</p>
  <p>piyo: {{ piyo }}</p>
  <div>
    <button @click="add">
      add
    </button>
  </div>
</template>

hogeの値を2倍したのがfugafugaに1足したのがpiyoです。

ボタンをクリックするたびhogeがインクリメントしてその他の値も変わるというコンポーネントです。

これをComposition APIで書くとこうなります。

<script setup>
import { ref, computed } from 'vue';
const hoge = ref(1);

const fuga = computed(() => {
  return hoge.value * 2;
});

const piyo = computed(() => {
  return fuga.value + 1;
});

const add = () => {
  hoge.value++;
}
</script>

<template>
  <p>hoge: {{ hoge }}</p>
  <p>fuga: {{ fuga }}</p>
  <p>piyo: {{ piyo }}</p>
  <div>
    <button @click="add">
      add
    </button>
  </div>
</template>

refcomputedRef<T>型のrefオブジェクトを返すので.valueを付けないと値にアクセスできません。

ただ、templateの中では.valueを付けなくても値を取り出せます。最初ここで詰まりました。今でもしっくり来ていません。

加えて、defineProps()で定義するprops.valueを付けなくても<script setup>内で値にアクセスできます。なんで。

リアクティブな値はrefオブジェクトなので.valueが必要、そうでない値はそのままでアクセス可能ということでしょう。

そしてNuxt3のuseAsyncData()の返り値のプロパティであるdataも実はrefオブジェクトです。

型定義を見てみると以下のようになっています。

function useAsyncData(
  handler: (nuxtApp?: NuxtApp) => Promise<DataT>,
  options?: AsyncDataOptions<DataT>
): AsyncData<DataT>
function useAsyncData(
  key: string,
  handler: (nuxtApp?: NuxtApp) => Promise<DataT>,
  options?: AsyncDataOptions<DataT>
): Promise<AsyncData<DataT>>

type AsyncDataOptions<DataT> = {
  server?: boolean
  lazy?: boolean
  default?: () => DataT | Ref<DataT> | null
  transform?: (input: DataT) => DataT
  pick?: string[]
  watch?: WatchSource[]
  immediate?: boolean
}

interface RefreshOptions {
  dedupe?: boolean
}

type AsyncData<DataT, ErrorT> = {
  data: Ref<DataT | null>
  pending: Ref<boolean>
  execute: () => Promise<void>
  refresh: (opts?: RefreshOptions) => Promise<void>
  error: Ref<ErrorT | null>
}

確かにdataの型はRef<DataT | null>となっています。

なのでこれも値を取り出そうとするときは.valueを付けないといけません。

useAsyncData()APIを叩いてデータを取得、computedでそのデータを元に加工して違う値を返す、なんてユースケースはよくあると思いますが、その時もdata.valueとしないといけません。

これドキュメントにも書いておらず、また色んな解説記事も読みましたが書いてある記事は無かったように思います(見逃していたらすみません)。

リクエストパラメータ

Nuxt3ではNuxt2でモジュールとして提供されていたaxiosに代わってofetchが入っています。

useFetch()useAsyncData()も内部ではこのofetchを使った$fetch()が使われています。

で、この$fetch()なんですがリクエストの際のパラメータの設定で詰まりました。

従来のNuxt2でGETリクエストの際はURLSearchParamsを使っていたのですが、それだとクエリパラメータが上手く設定されませんでした。

Githubのissuesを見てみると違う内容の質問ですがこんなのがあったのでこのように対応しました。

export const useCustomFetch = $fetch.create({
  onRequest({ options }) {
    if (options.params) {
      options.params = Object.fromEntries(options.params);
    }
  },
  // 後略
});

useCustomFetchというcomposableを作成しました。

Object.fromEntries()URLSearchParamsからオブジェクトに変換して渡しています。これでGETリクエストはOKです。

POSTリクエストの場合はFormDataを使っていたのですが、こちらはそのままでも使えました。

この違いは何なんでしょうか...。

まとめ

以上、Nuxtアップデートに関連して詰まったところを挙げてみました。

個人的にはやっぱりまだしっくり来ていないところなんですが、とはいえcomposablesにロジックを切り出せるのは良いところだなとも感じるのでメリット・デメリットともにある印象です。

だいぶゴールが見えてきているので最後までしっかりやりきりたいと思います。