Angular Advent Calendar 2 日目の記事です。 この記事は「E2E テストって名前は知ってるけど、やり方知らないし敷居が高いなー…」なんて思っている方に向けてツールの導入方法および簡単なサンプルを紹介するものです。かくいう私も Angular で E2E テストを書くのは初めてのため、手探りで調べながら記事を書きました。

この記事を書いた環境

  • Node 14.8
  • Angular 11

この記事のコードは GitHub でご覧いただけます。導入の際の参考になれば幸いです。 https://github.com/ringtail003/blog-angular-e2e

1)はじめに

E2E テストの目的

テストといえば、最もなじみのあるのが「ユニットテスト」ではないでしょうか。Angular でも $ ng generate component {コンポーネント名} と CLI でコンポーネントを作成すると、ユニットテストの雛形のファイルが生成されますね。

ユニットテストは単体のコンポーネントやサービスをテスト対象 とします。最もユースケースの多い正常な挙動に加え、データが極端に多いケースや空っぽのケース、例外が発生するケースなど、さまざまなバリエーションを与えてコンポーネント/サービスの挙動をチェックしていきます。単体のテスト対象にフォーカスするため、コンストラクタで注入する他のサービスや子コンポーネントはモックに差し替える事が多いでしょう。そして、コンポーネント/サービスを新規開発する時、変更を加える時など 実装コードと同時進行でテストを書いていくのが特徴 です。

それと比較して E2E テストは、完成したパーツ(コンポーネントやサービス)を実稼働に近い状態でテスト します。Web システムの場合はブラウザを開いてエンドユーザーの操作を模倣するスタイルが多く、コンポーネント/サービスは実装が完了している事が前提となります。

私が仕事で開発しているプロジェクトでは、作業にある程度のまとまりがついた段階でステージングサーバーにデプロイし、そこで動作確認を行って問題がなければ本番環境にデプロイしリリース、という運用をしています。ステージングサーバーでの動作確認は自分でページを操作しながら全機能を目視確認する「人力テスト作業」です。機能が増えるにつれ人力テスト作業の時間は増え続け、ここ最近では 2 時間以上かかるようになってしまいました。正直しんどい。やっている操作は毎回ほぼ同じなので、操作をシナリオに書き起こして自動化したい。 これが私の考える E2E テストの目的です。

E2E テストフレームワーク

E2E テスト専用のフレームワークってちょっと調べただけでも結構な数がありました。GitHub のスター数が多いのはこんなところでしょうか。

名前 Web サイト 配布元 リポジトリ GitHub star
Puppeteer Official npm GitHub 66.4 K
cypress Official npm GitHub 24.3 K
Nightwatch.js Official npm GitHub 10.5 K
Protractor Official npm GitHub 8.7 K
testcafe Official npm GitHub 8.6 K

さてさてどれを使って良いのやら…。今回は 「何となく聞いたことあるから使ってみたい」という独断で ●Protractor ●Puppeteer ●cypress の 3 つをお試しで使ってみようと思います。

2)テスト対象のデモアプリ

E2E テストの対象として、このような簡単なデモアプリを作ってみました。

ユーザー名が ringtail003 ならログインできます。パスワードは何でも通ります。ガバガバアプリです。

E2E でテストしたい内容(= シナリオ)は下記の通りです。

  1. 正常ケースのログイン/ログアウト
    • ユーザー名 ringtail003 でログインするとプロフィールのページに遷移する
    • プロフィールのページでログアウトする
  2. 異常ケースのログイン
    • エラーメッセージが表示されログインできない

3)Angular のサンプルテスト掘り下げ編

実は Angular では E2E の雛形のテストファイルとそれを動かす仕組みがビルトインで提供されています。CLI で $ ng new {プロジェクト名} した時、ルートディレクトリ直下に e2e というフォルダがあるのを見つけた人、いませんか?

E2E テストフレームワーク導入の前に、まずはサンプルのテストを動かして Angular の E2E テストがどのように動いているのか、構造を把握しておきましょう。

サンプルテストを叩く

デモアプリのプロジェクトで $ ng e2e コマンドを叩いてみます。サクッとテストが落ちます。

サンプルテストのコードを掘り下げる

/e2e/src を見てみましょう。ここにテストコードがあります。

このフォルダの tsconfig.json には types: ["jasmine"] が宣言されているためユニットテストでもおなじみの describe や it を使う事ができます。

サンプルの E2E テストはアプリケーションのルートコンポーネントである <app-root> にレンダリングされた内容を検証しています。デモアプリでレンダリング内容を変更してしまったので E2E テストが落ちたという訳です。

ng e2e を掘り下げる

$ ng e2e コマンドの動作は angular.json に宣言されています。ここを見ると TypeScript のテストコードを @angular-devkit/build-angular:protractor でビルドして、設定ファイルに何やら protractor.conf.js をというファイルを指定しているようです。

package.json を見ると E2E テストフレームワークの protractor への依存が定義されていますね。

Angular の $ ng e2e コマンドは Protractor を使った E2E テストだという事が分かりました。

protractor.conf.js を掘り下げる

次は e2e/protractor.conf.js を覗いてみましょう。

どうやら Chrome で http://localhost:4200/ を起動しているようです。

directConnect
Protractor が内包する Selenium を介さず PC にインストールされたブラウザを探し出して起動するオプションです。

framework
jasmine のコードを読み込んで describe や it が error TS2304: Cannot find name 'xxx'. エラーにならないようにしています。

Selenium を掘り下げる

Selenium はブラウザを使ったテストツールを提供するフレームワークです。ブラウザ操作の API である WebDriver を利用しているため $ ng e2e は E2E テストの開始直前に WebDriver をアップデートします。

$ ng e2e が吐き出すテキストを見ると node_modules 配下で何やらドライバをせっせとダウンロードしていますね。

ドライバのアップデートは $ ng e2e の度に実行されますが、ブラウザの新しいバージョンが出ない限りドライバ自体はそう頻繁に更新されません。そこで --no-webdriver-update オプションを渡す事でアップデートをスキップする事ができます。

Tips
$ npm ci した直後など WebDriver が一度もアップデートされていない状態で --no-webdriver-update オプションを使うと Error: Could not find update-config.json. というエラーが発生します。その際はオプションを外すか、手動で WebDriver をアップデートする必要があります。

# 手動で WebDriver をアップデート
$ ./node_modules/.bin/webdriver-manager update

4)E2E テスト準備編

WebAPI のモック

通常、ローカル開発には http://localhost:4200 を使いますが、これとは別に E2E テスト専用のエンドポイントを用意しておくと便利 です。レスポンスのモックデータも E2E テスト専用のものを用意しておきます。そうする事で実稼働のサーバーサイドの API やローカル開発環境用のモックデータと分離できて E2E テストが独立した外からの影響を受けにくいもの になります。

E2E テスト専用のビルトインサーバーでオススメなのは angular-in-memory-web-api です。これを使うと E2E のテストコードとモックデータをひとつの Angular アプリに集約する事ができます。そしてアプリと同じプロセスで動くので、サーバーの起動を待ったり、うまく動かない時にログをどこかに吐き出したりといった面倒な事から解放されます。

angular-in-memory-web-api で E2E 専用のモックを作る手順は下記の通りです。

  1. npm install
  2. E2E テスト専用のモックデータを宣言
  3. “e2e” という環境を environment で宣言
  4. angular.json で “e2e” の “fileReplacements” を定義
  5. “e2e” テスト環境で API レスポンスをモックに置き換え
  6. “http://localhost:4201” を E2E テスト用のエンドポイントとして定義
  7. E2E テスト環境を一発で起動できるように package.json にコマンド追加

さてこれで $ npm run start:e2e と叩くと http://localhost:4201 で E2E テスト専用の環境が立ち上がるようになりました。必要であればブラウザ越しに人力テスト作業をする事もできます。

5)導入編

さて、それではいよいよ E2E テストフレームワーク導入をしてみましょう。

この記事では e2e/protractor e2e/puppeteer e2e/cypress というフレームワークごとのフォルダを配置していきます。紛らわしいので雛形のテストコード e2e/src フォルダは削除しておきましょう。

Protractor

サポートブラウザ

名前 サポート
Chrome
FireFox
Safari
Edge -
その他 IE をサポート

https://www.protractortest.org/#/browser-support

導入する手順

  1. tsconfig.json を設定する
  2. angular.json に tsconfig.json を設定追加する
  3. conf.js に protractor の設定を記述する
  4. テストを作成する
  5. “npm run e2e:protractor” で E2E テストを実行する

補足
angular-in-memory-web-api のモックが不要であれば、上記の手順は全て不要です。その場合は e2e/src 配下のテストを書き換えて $ ng e2e するだけです。

protractor のテストコード

import { $, $$, browser, by, logging } from "protractor";

describe("login-valid", () => {
  const baseUrl = "http://localhost:4201/";

  it("有効なユーザーでログインしログアウトできる事", async () => {
    // ログインページを開く
    browser.get(`${baseUrl}login`);

    // ログイン
    $("form input[name='username']").sendKeys("ringtail003");
    $("form input[name='password']").sendKeys("***");
    $("form").submit();

    // プロフィールのページに遷移した事を確認
    browser.getCurrentUrl().then((url) => {
      expect(url).toBe(`${baseUrl}profile`);
    });

    // プロフィールのページにレンダリングされた内容をテスト
    expect(await $$("fieldset").all(by.css("span")).getText()).toEqual([
      "user@example.com",
      "ringtail003",
      "2020-12-02 10:15",
    ]);

    // ログアウト
    $("form").submit();

    // ログインページに遷移した事を確認
    browser.getCurrentUrl().then((url) => {
      expect(url).toBe(`${baseUrl}login`);
    });
  });

  afterEach(async () => {
    // 省略
  });
});

protractor の使い方

browser オブジェクトを利用してブラウザでページを開きます。

browser.get("http://localhost:4201/login");

element(by.css()) または $ を利用すると DOM の単体の要素にアクセスできます。

$("form input[name='password']").sendKeys("***");
$("form").submit();

element.all(by.css()) または $$ を利用すると DOM の要素を配列として取得できます。

$$("fieldset").all(by.css("span")).getText();

公式ドキュメントでは by.repeater などビルドされる前の HTML を指定する方法が書いてあるのですが、残念ながらこれは AngularJS の記法で Angular では利用できません。

<!-- AngularJS の記法 -->
<div ng-repeat="cat in pets">
  <span>{{cat.name}}</span>
  <span>{{cat.age}}</span>
</div>
var firstCatName = element(
  by.repeater("cat in pets").row(0).column("cat.name")
);

スクリーンショットを撮る方法もありますが、まあまあ自力な感じのコードですね。 https://www.protractortest.org/#/api?view=webdriver.WebElement.prototype.takeScreenshot

function writeScreenShot(data, filename) {
  var stream = fs.createWriteStream(filename);
  stream.write(new Buffer(data, "base64"));
  stream.end();
}

var foo = element(by.id("foo"));
foo.takeScreenshot().then((png) => {
  writeScreenShot(png, "foo.png");
});

Pros

Protractor の良さは何と言っても導入の簡単 さです。ビルトインで E2E テスト環境が提供されているため angular-in-memory-web-api で紹介したような API レスポンスのモックが不要であれば アプリ付属のサンプルテストコードを書き換えるだけで簡単に動きます

Cons

E2E のテスト内容が簡単なシナリオであれば良いのですが、スクリーンショットのコードを見て分かるように簡単便利な API はあまり提供されていません。Protractor で提供されていない API は WebDriver の API リファレンスを探して直接ドライバを叩くようなコードを書く事になります。がっつりシナリオを書いて細かくテストする場合には不向き だと言えます。

Puppeteer

サポートブラウザ

名前 サポート
Chrome
FireFox ●(experimental)
Safari -
Edge -
その他  

導入する手順

  1. npm install
  2. tsconfig.json を設定する
  3. angular.json に tsconfig.json を設定追加する
  4. conf.js に Puppeteer の設定を記述する
  5. capture フォルダを追加して Git 管理外にする
  6. テストを作成する
  7. “npm run e2e:puppeteer” で E2E テストを実行する

補足
通常 puppeteer はテストコードを JavaScript で書いて $ node test.js のように呼び出します。またその際、テスト対象の http://localhost:4201 が起動している必要があります。
上記の導入手順はビルトインの Protractor の仕組みを利用して TypeScript のトランスパイルからアプリの起動まで全てを Angular に任せたものです。必要な部分だけ後から差し替えできるよう設計されているのが Angular の良いところですね。

補足その2
Puppeteer には puppeteerpuppeteer-core のパッケージがあります。 前者の puppeteer パッケージのほうは Chromium.app (Chrome のベースとなるオープンソースのブラウザ)のバイナリをダウンロードしますが、これが 306MB とまあまあ巨大でした。CI で実行する事を考えると大きなファイルのダウンロードは避けたいので、後者の puppeteer-core パッケージのほうを使ったほうが良いでしょう。

puppeteer のテストコード

※ コードが長いので一部を関数にして helper ディレクトリに分離しています。

import { Page } from "puppeteer-core";
import { browser } from "./helper/browser";
import { screenshot } from "./helper/capture";
import { emulate } from "./helper/device";

describe("login-valid", () => {
  it("有効なユーザーでログインしログアウトできる事", (done: DoneFn) => {
    (async () => {
      // ブラウザの起動
      const sandbox = await browser.launch();

      // ログインページを開く
      const page: Page = await sandbox.newPage();
      emulate(page);
      await page.goto(`${browser.baseUrl}login`);

      // ログイン情報の入力
      await page.type("[name='username']", "ringtail003");
      await page.type("[name='password']", "***");

      // ログイン
      await page.click("form [type='submit']");

      // プロフィールのページに遷移した事を確認
      await page.waitForNavigation();
      expect(page.url()).toBe(`${browser.baseUrl}profile`);

      // プロフィールのページにレンダリングされた内容をテスト
      const fieldset = await page.$$("fieldset");
      const span = fieldset.map(async (v) =>
        v.$eval("span", (node) => node.innerHTML)
      );

      expect(await Promise.all(span)).toEqual([
        "user@example.com",
        "ringtail003",
        "2020-12-02 10:15",
      ]);

      // キャプチャを撮る
      await screenshot(page, "ログイン完了");

      // ログアウト
      const submit = await page.$("form [type='submit']");
      submit?.click();

      // ログインページに遷移した事を確認
      await page.waitForNavigation();
      expect(page.url()).toBe(`${browser.baseUrl}login`);

      // キャプチャを撮る
      await screenshot(page, "ログアウト完了");

      await sandbox.close();
    })().then(done);
  });
});

puppeteer の使い方

puppeteer.launch でブラウザを起動します。

await puppeteer.launch({
  // true にすると起動したブラウザに何もレンダリングされない、CI で実行する時は true にする
  headless: false,
  // 500 など設定するとブラウザ操作のスピードが緩くなり目視確認がやりやすい
  slowMo: 50,
  // デバッグウインドウの起動
  devtools: false,
  // PC にインストール済みのブラウザのパスを渡す
  executablePath:
    "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
});

goto でページを開きます。

const page: Page = await puppeteer.launch({ ... });
await page.goto(`http://localhost:4201/login`);

emulate を使うとブラウザのデバイスをエミュレートできます。 デバイスはの定義は puppeteer パッケージに定義されたものを参考にしました。
https://github.com/puppeteer/puppeteer/blob/v5.4.1/src/common/DeviceDescriptors.ts

page.emulate({
  name: "iPhone X",
  userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac ...",
  viewport: {
    width: 375,
    height: 812,
    deviceScaleFactor: 3,
    isMobile: true,
    hasTouch: true,
    isLandscape: false,
  },
});

エミュレートしたデバイスでスクリーンショットが簡単に撮れます。

await page.screenshot({
  path: `${__dirname}/../capture/テスト.png`,
  fullPage: true,
});

ヘッドレス Chrome の仕様でデフォルトフォントが明朝体になってしまうので、スクリーンショットのためにアプリ全体にフォントを適用したほうが良さそうです。

/* src/styles.scss */

body {
  font-family: "Helvetica Neue", Arial, "Hiragino Kaku Gothic ProN",
    "Hiragino Sans", Meiryo, sans-serif;
}

フォント指定については下記サイトをを参考にさせていただきました。
2020 年に最適な font-family の書き方

Pros

導入は簡単で E2E テストに必要なブラウザ操作についても一通りの機能が揃っている印象です。

公式ドキュメント もシンプルで程よいボリュームです。API を探して巨大なドキュメントをさまようドキュメント難民にならずに済みそう です。

Cons

テストコードを見て分かる通り async await の嵐です。試しにコードを書いてみた程度ですがすでに 非同期処理との戦い が始まっていて、複数デバイスをエミュレートし連続でキャプチャを撮れないかと苦戦した結果あきらめました。

その他

ブラウザ操作を記録して puppeteer のテストコードを吐き出す Headless Recorder という Chrome エクステンションがあったので試してみました。

DOM の階層を忠実に辿りすぎて .ng-untouched などを拾ってしまうため、吐き出したコードをそのまま E2E テストとして使うにはちょっと苦しい感じでした。puppeteer は API がシンプルなので、簡単な操作であれば自分で書いたほうが読みやすいすっきりしたテストコードになりそうです。

const puppeteer = require('puppeteer');
(async () => {
  const browser = await puppeteer.launch()
  const page = await browser.newPage()
  const navigationPromise = page.waitForNavigation()

  await page.goto('http://localhost:4201/login')
  await page.setViewport({ width: 1280, height: 878 })
  await navigationPromise
  await navigationPromise

  await page.waitForSelector('app-login > .ng-untouched > .container > fieldset:nth-child(1) > .ng-untouched')
  await page.click('app-login > .ng-untouched > .container > fieldset:nth-child(1) > .ng-untouched')
  await page.waitForSelector('app-login > .ng-invalid > .container > fieldset > .ng-untouched')
  await page.click('app-login > .ng-invalid > .container > fieldset > .ng-untouched')
  ...

  await browser.close()
})()

cypress

サポートブラウザ

名前 サポート
Chrome
FireFox
Safari -
Edge
その他 Electron をサポート

https://docs.cypress.io/guides/guides/launching-browsers.html#Browsers

導入する手順

  1. npm install
  2. tsconfig.json を設定する
  3. angular.json に tsconfig.json を設定追加する
  4. cypress.json に設定を記述する
  5. supports フォルダのファイルを作成する
  6. plugins フォルダのファイルを作成する
  7. fixtures フォルダのファイルを作成する
  8. capture フォルダを追加して Git 管理外にする
  9. integration フォルダにテストを作成する
  10. “npm run e2e:cypress” で E2E テストを実行する

補足
手順が多いように思うかもしれませんが $ npm install の後に $ ./node_modules/.bin/cypress open するとプロジェクトのルートディレクトリ直下に雛形が生成されます。そこからコピーすれば導入はとても簡単です。

補足その2
cypress-schematic を使うと $ ng add で cypress を簡単に導入できます。今回は仕組みを知りたかったので、あえて最初からインストールしました。

説明

cypress は TypeScript のトランスパイルやテストランナー全てひっくるめた、オールインワンのフレームワークです。そのため Angular ビルトインの E2E の仕組みではなく cypress を直接叩く必要があります。テスト実行のためのサブコマンド open は GUI のテストランナーを起動し run は CLI でテストを実行します。

これらをコマンド一発で起動するように package.json に追加しておきます。

// package.json
{
  "scripts": {
    ...
    "cypress:open": "cypress open --config-file ./e2e/cypress/cypress.json --browser chrome",
    "cypress:run": "cypress run --config-file ./e2e/cypress/cypress.json --browser chrome"
  }
}

また Angular アプリが起動してから open run する必要があるため start-server-and-test でアプリの起動完了を待ちます。これも package.json に追加しておきます。

// package.json
{
  "scripts": {
    ...
    "e2e:cypress": "start-server-and-test start:e2e http://localhost:4201 cypress:run",
    "e2e:cypress:ide": "start-server-and-test start:e2e http://localhost:4201 cypress:open"
  }
}

$ npm run e2e:cypress:ide を叩くと GUI のテストランナーが起動します。ここではブラウザを切り替えて 1 ファイルごとにテストを実行する事ができます。

$ npm run e2e:cypress を叩くと CLI でテストを実行します。

cypress のテストコード

context("login-valid", () => {
  it("有効なユーザーでログインしログアウトできる事", () => {
    cy.fixture("valid.json").then((fixture) => {
      cy.visit("/login");
      cy.get("[name='username']").type(fixture.username);
      cy.get("[name='password']").type(fixture.password);
      cy.get("form").get("footer").contains("ログイン").click();

      cy.wait(500);
      cy.location().should((location) => {
        expect(location.pathname).to.eq("/profile");
      });

      cy.contains("user@example.com");
      cy.contains("ringtail003");
      cy.contains("2020-12-02 10:15");

      cy.screenshot("ログイン完了");

      cy.get("form").get("footer").contains("ログアウト").click();

      cy.wait(500);
      cy.location().should((location) => {
        expect(location.pathname).to.eq("/login");
      });

      cy.screenshot("ログアウト完了");
    });
  });
});

cypress の使い方

テストコードは mochachai を使って書きます。

context("xxx", () => {
  it("yyy", () => {
    expect({}).to.eql({});
    expect(1).not.to.be(2);
  });
});

fixtures フォルダで宣言した json を cy.fixture で読み込む事ができます。

// fixtures/valid.json
{
  "username": "ringtail003",
  "password": "***"
}
cy.fixture("valid.json").then((fixture) => {
  fixture.usrname; // "ringtail003"
});

fixture から読み込んだデータは画面の入力値として使用する事ができます。データと振る舞いを分離できるのできれいですね。

cy.fixture("valid.json").then((fixture) => {
  cy.visit("/login");
  cy.get("[name='username']").type(fixture.username);
  cy.get("[name='password']").type(fixture.password);
  cy.get("form").get("footer").contains("ログイン").click();
});

Web アプリケーションの DOM のレンダリングは非同期ですが cypress では対象の DOM が見つかるまで待機してくれるようです。そのため await を使わず同期処理であるかのようにコードを書くことができます。また特定の DOM 検索のみタイムアウトを伸ばす事もできます。

cy.get(".my-slow-selector", { timeout: 10000 });

今回は使っていませんが supports フォルダのファイルを使うと cy.login() のようなカスタムコマンドを定義する事ができます。

// supports/command.js
Cypress.Commands.add("login", () => {
  cy.get(".footer").contains("ログイン").click();
});

cy.login();

npm で公開されたカスタムコマンドは plugins フォルダのファイルを拡張すれば取り込みができるようです(ドキュメントをチラ見しただけで試してません)。
https://docs.cypress.io/plugins/index.html

実は API レスポンスのモックも cypress で完結します。

cy.intercept("POST", "http://example.com/widgets", {
  statusCode: 200,
  body: "it worked!",
});

https://docs.cypress.io/api/commands/intercept.html#Comparison-to-cy-route

先に紹介した angular-in-memory-web-api を使った方法では cypress を立ち上げなくても http://localhost:4201 で人力テスト作業が可能で、例えば「ユーザー名 “foo” でログインすると初期設定が未完了のユーザーの画面を確認できる」という事もできるため E2E 環境構築の副産物としてローカル開発にも利用する事ができます。ですので、機能が用意されているからといってモックまで cypress に頼らなくてもいいかなあ、というのが正直なところです。

Pros

cypress は とにかく多機能 です。API が直感的でコードを書くのも楽でした。がっつり E2E テストを書くならオススメ のフレームワークと言えます。

Cons

公式ドキュメントのボリュームが非常に大きく 最初はどこから手をつけていいのか分からずドキュメント難民になりがち です。should contains など一見すると単なるメソッドのようにも見える独特のアサーションがあり、また mocha/chai についても少なからず知識が必要です。

まとめ

お試し導入してみた 3 つのフレームワークをまとめました。

フレームワーク 導入が簡単 テストの書きやすさ ドキュメントの調べやすさ 機能の充実 所感
Protractor ★★★ セットアップを簡単に済ませたい人向け
puppeteer ★★ ★★ ★★ そこそこの機能を求める人向け
cypress ★★★ ★★★ がっつりテスト書きたい人向け

結果、2 時間もの人力テスト作業を E2E テストで代行しようとしている私にとっては「がっつりテスト書きたい人向け」の cypress が最もマッチするフレームワークと言えそうです。

明日へのバトン

私は今の会社に入社した当初、テストはおろか JavaScript の配列とオブジェクトの違いすら分からない あんぽんたん でした🥺💧。ユニットテストを書いてくださいという指示に対して人力テスト用の /admin/test.html のようなページを作り、プルリクエストを速攻でリジェクトされたのを 根に持って 覚えています。それが数年後に E2E テストの紹介記事を書くようになるとは、何事も為せば成るものですね。

たびたび詰んで実装スピードが異常なほど遅い私でしたが「大丈夫です、いつか書けるから!👍」といつも背中を押してくれた上司がいました。会社でフロントエンドのフレームワーク選定をした時「Angular は設計のエッセンスが詰まった学びのフレームワークだと思う、エンジニアとして成長するために Angular を今後メインで使うフレームワークに据えたい」という私の意見に「僕もそう思います!!😃」と言って賛成してくれたのも、この上司でした。現在はやりたい事を実現するためフリーランスに転向し元上司となりましたが、今でも私の尊敬するエンジニアの一人です。

明日はそんな私の元上司である ✨📣 ttskch さんの投稿 📣✨ です。たつきちさん、よろしくお願いします!