LoginSignup
13
10

More than 5 years have passed since last update.

『テスト駆動開発』第1部をJavaScript(Node.js + AVA)で実習する

Last updated at Posted at 2017-10-16

書籍『テスト駆動開発』の第1部には、テスト駆動開発で多国通貨を実装するサンプルが載っています。このサンプルは丁寧に書かれていて参考になりますが、Java + JUnitなので、自分の馴染みのある言語+テスティングフレームワークではどうなるのか気になります。

そこで、本記事では、JavaScript(Node.js + AVA)で『テスト駆動開発』の第1部を実習します。

なお、本記事のコードはGitHubの下記リポジトリでも公開しています。
https://github.com/ryo-utsunomiya/tdd-js-ava

環境構築

Node.jsがインストール済みであることを前提にします。私の手元の環境は以下の通りです。

  • Node.js v8.7.0
  • npm 5.5.1

AVAのインストール

テスティングフレームワークはAVAを使用します。AVAはシンプルなAPIとモダンな機能を備えています。

npm init
npm install ava --save-dev
./node_modules/.bin/ava --init

AVAの動作確認として、1つテストを書いてみましょう。AVAはBabelによるトランスパイル機能を内蔵しているので、テストはES2015+で書くことができます。

// test.js
import test from 'ava';

test('foo', (t) => {
  t.pass();
});

実行はnpm testです。以下のような結果が表示されればAVAのセットアップは完了です。

setup ava

Babel設定

テストがES Modulesなのにコードの方はCommonJSなのは気持ち悪いので、コードの方もBabelでトランスパイルするよう設定します。package.jsonに以下を追記しましょう。

  "ava": {
    "require": [
      "babel-register"
    ]
  },
  "babel": {
    "presets": [
      "@ava/stage-4"
    ]
  }

次に、モジュールを読み込んで動作させるテストを書きます。

// test.js
import test from 'ava';
import foo from './foo';

test('test foo()', t => {
  t.is(foo(), 'foo');
});

モジュールの方も書きます。

export default () => 'foo';

もう一度実行してみましょう。今度はnpm tという短縮コマンドを使ってみるとよいでしょう。

「1 passed」と表示されればセットアップ完了です!

第1章

はじめにテストを書きます。ファイル名を「XX.test.js」とすると、AVAはそのファイルをテストとみなします。

// Money.test.js
import test from 'ava';

test('test multiplication', t => {
  const five = new Dollar(5);
  five.times(2);
  t.is(10, five.amount);
});

まだ Dollar オブジェクトは定義されていないので、ReferenceErrorが発生してテストは失敗します。

ReferenceErrorを解消するための最小限の実装をしてみます。

// Dollar.js
export default class Dollar {
  constructor(amount) {
    this.amount = amount;
  }

  times(multiplier) {

  }
}

次に、テストでこのファイルを読み込みます。

import test from 'ava';
import Dollar from './Dollar';

test('test multiplication', t => {
  const five = new Dollar(5);
  five.times(2);
  t.is(10, five.amount);
});

今度は、エラーではなくアサーションが失敗します。出力を見てみましょう。

スクリーンショット 2017-10-15 15.08.37.png

どこで失敗しているか、わかりやすく表示してくれます。AVAのアサーションには、『テスト駆動開発』訳者のt-wadaさん作のpower-assertが使われています。

コードを編集するたびに npm t を実行するのは面倒なので、npm t -- --watch で、コードの変更に応じてテストが実行されるようにしておくと便利です。

この辺で下準備が終わったので、以降は各章の進捗をまとめて載せていきます。詳細なステップは書籍を参照してください。

第1章完了時点では以下のようになります。

import test from 'ava';
import Dollar from './Dollar';

test('test multiplication', (t) => {
  const five = new Dollar(5);
  five.times(2);
  t.is(10, five.amount);
});
export default class Dollar {
  constructor(amount) {
    this.amount = amount;
  }

  times(multiplier) {
    this.amount *= multiplier;
  }
}

第2章

Dollar.times() のテストを増やし、テストをパスするように実装します。

import test from 'ava';
import Dollar from './Dollar';

test('test multiplication', (t) => {
  const five = new Dollar(5);
  t.is(10, five.times(2).amount);
  t.is(15, five.times(3).amount);
});
export default class Dollar {
  constructor(amount) {
    this.amount = amount;
  }

  times(multiplier) {
    return new Dollar(this.amount * multiplier);
  }
}

第3章

DollarをValueObjectにするために、オブジェクトの等価性を比較できるようにします。テストケースが複数になったので、asyncを導入して並列実行できるようにしましょう。

import test from 'ava';
import Dollar from './Dollar';

test('multiplication', async (t) => {
  const five = new Dollar(5);
  t.is(10, five.times(2).amount);
  t.is(15, five.times(3).amount);
});

test('equality', async (t) => {
  t.true(new Dollar(5).equals(new Dollar(5)));
});
export default class Dollar {
  constructor(amount) {
    this.amount = amount;
  }

  times(multiplier) {
    return new Dollar(this.amount * multiplier);
  }

  equals(object) {
    return this.amount === object.amount;
  }
}

第4章

この章では、オブジェクト同士を比較するよう、テストを改善します。しかし、JavaScriptには、JavaのObject.equalsのようなオブジェクトの等価性比較をオーバーライドするためのAPIはありません。そのため、本章のリファクタリングは適用できません。

第5章

新しくFrancオブジェクトを追加します。この時点では、テストも実装もコピペです。

import test from 'ava';
import Dollar from './Dollar';
import Franc from './Franc';

test('multiplication', async (t) => {
  const five = new Dollar(5);
  t.true(new Dollar(10).equals(five.times(2)));
  t.true(new Dollar(15).equals(five.times(3)));
});

test('franc multiplication', async (t) => {
  const five = new Franc(5);
  t.true(new Franc(10).equals(five.times(2)));
  t.true(new Franc(15).equals(five.times(3)));
});

test('equality', async (t) => {
  t.true(new Dollar(5).equals(new Dollar(5)));
});
export default class Franc {
  constructor(amount) {
    this.amount = amount;
  }

  times(multiplier) {
    return new Franc(this.amount * multiplier);
  }

  equals(object) {
    return this.amount === object.amount;
  }
}

第6章

ここからは、DollarとFrancの共通部分をMoneyに引き上げていきます。

test('equality', async (t) => {
  t.true(new Dollar(5).equals(new Dollar(5)));
  t.false(new Dollar(5).equals(new Dollar(6)));
  t.true(new Franc(5).equals(new Franc(5))); // テストケース追加
  t.false(new Franc(5).equals(new Franc(6))); // テストケース追加
});
export default class Money {
  constructor(amount) {
    this.amount = amount;
  }

  equals(money) {
    return this.amount === money.amount;
  }
}
import Money from './Money';

export default class Dollar extends Money {
  times(multiplier) {
    return new Dollar(this.amount * multiplier);
  }
}
import Money from './Money';

export default class Franc extends Money {
  times(multiplier) {
    return new Franc(this.amount * multiplier);
  }
}

第7章

DollarとFrancが等しくなってしまうバグがあるので、修正します。JavaScriptにはクラスはないので、コンストラクタの名前を比較することで代用しています。イマイチな実装ですが、後でリファクタリングするので…。

test('equality', async (t) => {
  t.true(new Dollar(5).equals(new Dollar(5)));
  t.false(new Dollar(5).equals(new Dollar(6)));
  t.true(new Franc(5).equals(new Franc(5)));
  t.false(new Franc(5).equals(new Franc(6)));
  t.false(new Franc(5).equals(new Dollar(5))); // テストケース追加
});
export default class Money {
  constructor(amount) {
    this.amount = amount;
  }

  equals(money) {
    return this.amount === money.amount
      && this.constructor.name === money.constructor.name;
  }
}

第8章

ここでは、timesメソッドをMoneyに引き上げる前準備として、DollarとFrancのFactory MethodをMoneyに作成します。

ここで問題発生。以下のような複数の循環参照があるモジュールでは、参照を解決できないのです。

import Dollar from './Dollar';
import Franc from './Franc';

export default class Money {
  static dollar() {
    return new Dollar();
  }
  static franc() {
    return new Franc();
  }
}
import Money from './Money';

export default class Dollar extends Money {}
import Money from './Money';

export default class Franc extends Money {}

全てのクラスを同一ファイル内に含めれば正常に実行できるので、これはモジュールのインポートの問題だと思います。

今後DollarとFrancは削除する予定なので、ひとまず、Money.jsにDollarとFrancを含めるようにして対応します。

import test from 'ava';
import Money from './Money';

test('multiplication', (t) => {
  const five = Money.dollar(5);
  t.true(Money.dollar(10).equals(five.times(2)));
  t.true(Money.dollar(15).equals(five.times(3)));
});

test('franc multiplication', (t) => {
  const five = Money.franc(5);
  t.true(Money.franc(10).equals(five.times(2)));
  t.true(Money.franc(15).equals(five.times(3)));
});

test('equality', (t) => {
  t.true(Money.dollar(5).equals(Money.dollar(5)));
  t.false(Money.dollar(5).equals(Money.dollar(6)));
  t.true(Money.franc(5).equals(Money.franc(5)));
  t.false(Money.franc(5).equals(Money.franc(6)));
  t.false(Money.franc(5).equals(Money.dollar(5)));
});
export default class Money {
  constructor(amount) {
    this.amount = amount;
  }

  equals(money) {
    return this.amount === money.amount
      && this.constructor.name === money.constructor.name;
  }

  static dollar(amount) {
    return new Dollar(amount);
  }

  static franc(amount) {
    return new Franc(amount);
  }
}

class Franc extends Money {
  times(multiplier) {
    return Money.franc(this.amount * multiplier);
  }
}

class Dollar extends Money {
  times(multiplier) {
    return Money.dollar(this.amount * multiplier);
  }
}

第9章

ここでは、通貨(currency)の概念を導入します。Javaでは Money.currency() はメソッド、 Money.currency はフィールドで併存可能ですが、JavaScriptでは併存できないので、Money.currencyはメソッドではなくプロパティにしています。

// currencyのテストを追加
test('currency', (t) => {
  t.is('USD', Money.dollar(1).currency);
  t.is('CHF', Money.franc(1).currency);
});
export default class Money {
  constructor(amount, currency) {
    this.amount = amount;
    this.currency = currency;
  }

  equals(money) {
    return this.amount === money.amount
      && this.constructor.name === money.constructor.name;
  }

  static dollar(amount) {
    return new Dollar(amount, 'USD');
  }

  static franc(amount) {
    return new Franc(amount, 'CHF');
  }
}

class Franc extends Money {
  times(multiplier) {
    return Money.franc(this.amount * multiplier);
  }
}

class Dollar extends Money {
  times(multiplier) {
    return Money.dollar(this.amount * multiplier);
  }
}

第10章

times() メソッドをMoneyクラスに引き上げ、同じ通貨かの比較はcurrencyプロパティを使うようにします。DollarとFrancを消す準備ができました。

import test from 'ava';
import { Money, Franc } from './Money';

test('multiplication', (t) => {
  const five = Money.dollar(5);
  t.true(Money.dollar(10).equals(five.times(2)));
  t.true(Money.dollar(15).equals(five.times(3)));
});

test('franc multiplication', (t) => {
  const five = Money.franc(5);
  t.true(Money.franc(10).equals(five.times(2)));
  t.true(Money.franc(15).equals(five.times(3)));
});

test('equality', (t) => {
  t.true(Money.dollar(5).equals(Money.dollar(5)));
  t.false(Money.dollar(5).equals(Money.dollar(6)));
  t.true(Money.franc(5).equals(Money.franc(5)));
  t.false(Money.franc(5).equals(Money.franc(6)));
  t.false(Money.franc(5).equals(Money.dollar(5)));
});

test('currency', (t) => {
  t.is(Money.dollar(1).currency, 'USD');
  t.is(Money.franc(1).currency, 'CHF');
});

test('different class equality', (t) => {
  t.true(new Money(10, 'CHF').equals(new Franc(10, 'CHF')));
});

export class Money {
  constructor(amount, currency) {
    this.amount = amount;
    this.currency = currency;
  }

  equals(money) {
    return this.amount === money.amount
      && this.currency === money.currency;
  }

  times(multiplier) {
    return new Money(this.amount * multiplier, this.currency);
  }


  static dollar(amount) {
    return new Money(amount, 'USD');
  }

  static franc(amount) {
    return new Money(amount, 'CHF');
  }
}

export class Franc extends Money {
}

export class Dollar extends Money {
}

第11章

DollarとFrancはもう不要です。消してしまいましょう。

import test from 'ava';
import Money from './Money';

test('multiplication', (t) => {
  const five = Money.dollar(5);
  t.true(Money.dollar(10).equals(five.times(2)));
  t.true(Money.dollar(15).equals(five.times(3)));
});

test('equality', (t) => {
  t.true(Money.dollar(5).equals(Money.dollar(5)));
  t.false(Money.dollar(5).equals(Money.dollar(6)));
  t.false(Money.franc(5).equals(Money.dollar(5)));
});

test('currency', (t) => {
  t.is(Money.dollar(1).currency, 'USD');
  t.is(Money.franc(1).currency, 'CHF');
});
export default class Money {
  constructor(amount, currency) {
    this.amount = amount;
    this.currency = currency;
  }

  equals(money) {
    return this.amount === money.amount
      && this.currency === money.currency;
  }

  times(multiplier) {
    return new Money(this.amount * multiplier, this.currency);
  }


  static dollar(amount) {
    return new Money(amount, 'USD');
  }

  static franc(amount) {
    return new Money(amount, 'CHF');
  }
}

第12章

ここからは、異なる通貨の加算を実装します。まず、同一通貨の加算のテストを書きます。

import test from 'ava';
import Money from './Money';
import Bank from './Bank';

test('multiplication', (t) => {
  const five = Money.dollar(5);
  t.true(Money.dollar(10).equals(five.times(2)));
  t.true(Money.dollar(15).equals(five.times(3)));
});

test('equality', (t) => {
  t.true(Money.dollar(5).equals(Money.dollar(5)));
  t.false(Money.dollar(5).equals(Money.dollar(6)));
  t.false(Money.franc(5).equals(Money.dollar(5)));
});

test('currency', (t) => {
  t.is(Money.dollar(1).currency, 'USD');
  t.is(Money.franc(1).currency, 'CHF');
});

test('simple addition', (t) => {
  const five = Money.dollar(5);
  const sum = five.plus(five);
  const bank = new Bank();
  const reduced = bank.reduce(sum, 'USD');
  t.true(reduced.equals(Money.dollar(10)));
});

書籍では、ここでExpressionというインタフェースを導入しています。しかし、JavaScriptにはインタフェースはありません。クラスで代用するのも違和感があったのと、この時点ではExpressionインタフェースは何の仕事もしないので、ひとまずExpressionは無視してBankだけ仮実装します。

export default class Money {
  constructor(amount, currency) {
    this.amount = amount;
    this.currency = currency;
  }

  equals(money) {
    return this.amount === money.amount
      && this.currency === money.currency;
  }

  times(multiplier) {
    return new Money(this.amount * multiplier, this.currency);
  }

  plus(addend) {
    return new Money(this.amount + addend.amount, this.currency);
  }

  static dollar(amount) {
    return new Money(amount, 'USD');
  }

  static franc(amount) {
    return new Money(amount, 'CHF');
  }
}
import Money from './Money';

export default class Bank {
  reduce(source, to) {
    return Money.dollar(10);
  }
}

第13章

加算処理を表すクラスを追加していきます。

// テスト追加
test('plus returns Sum', (t) => {
  const five = Money.dollar(5);
  const sum = five.plus(five);
  t.is(five, sum.augend);
  t.is(five, sum.addend);
});

test('reduce sum', (t) => {
  const sum = new Sum(Money.dollar(3), Money.dollar(4));
  const bank = new Bank();
  const result = bank.reduce(sum, 'USD');
  t.true(Money.dollar(7).equals(result));
});

test('reduce money', (t) => {
  const bank = new Bank();
  const result = bank.reduce(Money.dollar(1), 'USD');
  t.true(result.equals(Money.dollar(1)));
});

本章の主役、Sumクラスを追加します。

import Money from './Money';

export default class Sum {
  constructor(augend, addend) {
    this.augend = augend;
    this.addend = addend;
  }

  reduce(to) {
    const amount = this.augend.amount + this.addend.amount;
    return new Money(amount, to);
  }
}

Bank.reduce()は渡されたもののreduce()メソッドを呼ぶようにします。ここには、SumまたはMoneyを渡す想定です。

import Money from './Money';

export default class Bank {
  reduce(source, to) {
    return source.reduce(to);
  }
}

Money.reduce() を追加します。

import Sum from './Sum';

export default class Money {
  constructor(amount, currency) {
    this.amount = amount;
    this.currency = currency;
  }

  equals(money) {
    return this.amount === money.amount
      && this.currency === money.currency;
  }

  times(multiplier) {
    return new Money(this.amount * multiplier, this.currency);
  }

  plus(addend) {
    return new Sum(this, addend);
  }

  reduce() {
    return this;
  }

  static dollar(amount) {
    return new Money(amount, 'USD');
  }

  static franc(amount) {
    return new Money(amount, 'CHF');
  }
}

第14章

通貨の交換レートを実装していきます。

// テスト追加
test('reduce money different currency', (t) => {
  const bank = new Bank();
  bank.addRate('CHF', 'USD', 2);
  const result = bank.reduce(Money.dollar(1), 'USD');
  t.true(result.equals(Money.dollar(1)));
});

test('identity rate', (t) => {
  t.is(new Bank().rate('USD', 'USD'), 1);
});

レートの取得処理はいったんオブジェクトで実装します。あとでMap等に変更するかもしれません。

export default class Bank {
  constructor() {
    this.rates = {};
  }

  reduce(source, to) {
    return source.reduce(this, to);
  }

  addRate(from, to, rate) {
    this.rates[from + to] = rate;
  }

  rate(from, to) {
    if (from === to) return 1;
    return this.rates[from + to];
  }
}

reduceメソッドの第1引数にBankオブジェクトを渡して、交換レートを取得できるようにします。

import Sum from './Sum';

export default class Money {
  constructor(amount, currency) {
    this.amount = amount;
    this.currency = currency;
  }

  equals(money) {
    return this.amount === money.amount
      && this.currency === money.currency;
  }

  times(multiplier) {
    return new Money(this.amount * multiplier, this.currency);
  }

  plus(addend) {
    return new Sum(this, addend);
  }

  reduce(bank, to) {
    const rate = bank.rate(this.currency, to);
    return new Money(this.amount / rate, to);
  }

  static dollar(amount) {
    return new Money(amount, 'USD');
  }

  static franc(amount) {
    return new Money(amount, 'CHF');
  }
}
import Money from './Money';

export default class Sum {
  constructor(augend, addend) {
    this.augend = augend;
    this.addend = addend;
  }

  reduce(bank, to) {
    const amount = this.augend.amount + this.addend.amount;
    return new Money(amount, to);
  }
}

第15章

本章では、異なる通貨の加算処理を完成させ、Expressionインタフェースの拡充を行っています。が、本記事ではここまでExpressionインタフェース相当のものを実装しておらず、必要性も感じないので、このまま進みます。

// テストケース追加
test('mixed addition', (t) => {
  const fiveBucks = Money.dollar(5);
  const tenFrancs = Money.franc(10);
  const bank = new Bank();
  bank.addRate('CHF', 'USD', 2);
  const result = bank.reduce(fiveBucks.plus(tenFrancs), 'USD');
  t.true(result.equals(Money.dollar(10)));
});
import Money from './Money';

export default class Sum {
  constructor(augend, addend) {
    this.augend = augend;
    this.addend = addend;
  }

  reduce(bank, to) {
    const amount = this.augend.reduce(bank, to).amount +
      this.addend.reduce(bank, to).amount;
    return new Money(amount, to);
  }
}

第16章

Sum.times()メソッドを実装して仕上げです。また、テストのasyncが途中から消えていたので、改めてつけ直しています。

import test from 'ava';
import Money from './Money';
import Bank from './Bank';
import Sum from './Sum';

test('multiplication', async (t) => {
  const five = Money.dollar(5);
  t.true(Money.dollar(10).equals(five.times(2)));
  t.true(Money.dollar(15).equals(five.times(3)));
});

test('equality', async (t) => {
  t.true(Money.dollar(5).equals(Money.dollar(5)));
  t.false(Money.dollar(5).equals(Money.dollar(6)));
  t.false(Money.franc(5).equals(Money.dollar(5)));
});

test('currency', async (t) => {
  t.is(Money.dollar(1).currency, 'USD');
  t.is(Money.franc(1).currency, 'CHF');
});

test('simple addition', async (t) => {
  const five = Money.dollar(5);
  const sum = five.plus(five);
  const bank = new Bank();
  const reduced = bank.reduce(sum, 'USD');
  t.true(reduced.equals(Money.dollar(10)));
});

test('plus returns Sum', async (t) => {
  const five = Money.dollar(5);
  const sum = five.plus(five);
  t.is(five, sum.augend);
  t.is(five, sum.addend);
});

test('reduce sum', async (t) => {
  const sum = new Sum(Money.dollar(3), Money.dollar(4));
  const bank = new Bank();
  const result = bank.reduce(sum, 'USD');
  t.true(Money.dollar(7).equals(result));
});

test('reduce money', async (t) => {
  const bank = new Bank();
  const result = bank.reduce(Money.dollar(1), 'USD');
  t.true(result.equals(Money.dollar(1)));
});

test('reduce money different currency', async (t) => {
  const bank = new Bank();
  bank.addRate('CHF', 'USD', 2);
  const result = bank.reduce(Money.dollar(1), 'USD');
  t.true(result.equals(Money.dollar(1)));
});

test('identity rate', async (t) => {
  t.is(new Bank().rate('USD', 'USD'), 1);
});

test('mixed addition', async (t) => {
  const fiveBucks = Money.dollar(5);
  const tenFrancs = Money.franc(10);
  const bank = new Bank();
  bank.addRate('CHF', 'USD', 2);
  const result = bank.reduce(fiveBucks.plus(tenFrancs), 'USD');
  t.true(result.equals(Money.dollar(10)));
});

test('sum plus money', async (t) => {
  const fiveBucks = Money.dollar(5);
  const tenFrancs = Money.franc(10);
  const bank = new Bank();
  bank.addRate('CHF', 'USD', 2);
  const sum = new Sum(fiveBucks, tenFrancs).plus(fiveBucks);
  const result = bank.reduce(sum, 'USD');
  t.true(result.equals(Money.dollar(15)));
});

test('sum times', async (t) => {
  const fiveBucks = Money.dollar(5);
  const tenFrancs = Money.franc(10);
  const bank = new Bank();
  bank.addRate('CHF', 'USD', 2);
  const sum = new Sum(fiveBucks, tenFrancs).times(2);
  const result = bank.reduce(sum, 'USD');
  t.true(result.equals(Money.dollar(20)));
});

最終形なので、変更のないMoneyとBankも再掲します。

import Sum from './Sum';

export default class Money {
  constructor(amount, currency) {
    this.amount = amount;
    this.currency = currency;
  }

  equals(money) {
    return this.amount === money.amount
      && this.currency === money.currency;
  }

  times(multiplier) {
    return new Money(this.amount * multiplier, this.currency);
  }

  plus(addend) {
    return new Sum(this, addend);
  }

  reduce(bank, to) {
    const rate = bank.rate(this.currency, to);
    return new Money(this.amount / rate, to);
  }

  static dollar(amount) {
    return new Money(amount, 'USD');
  }

  static franc(amount) {
    return new Money(amount, 'CHF');
  }
}
export default class Bank {
  constructor() {
    this.rates = {};
  }

  reduce(source, to) {
    return source.reduce(this, to);
  }

  addRate(from, to, rate) {
    this.rates[from + to] = rate;
  }

  rate(from, to) {
    if (from === to) return 1;
    return this.rates[from + to];
  }
}
import Money from './Money';

export default class Sum {
  constructor(augend, addend) {
    this.augend = augend;
    this.addend = addend;
  }

  reduce(bank, to) {
    const amount = this.augend.reduce(bank, to).amount +
      this.addend.reduce(bank, to).amount;
    return new Money(amount, to);
  }

  plus(addend) {
    return new Sum(this, addend);
  }

  times(multiplier) {
    return new Sum(this.augend.times(multiplier), this.addend.times(multiplier));
  }
}

感想

Node + AVAだと手軽にテストが書けるので、『テスト駆動開発』の実習にはもってこいの環境でした。設計面で、Expressionインタフェースの扱いをどうするか迷いましたが、素のJavaScriptにインタフェースはないので、ダックタイピング的な設計でいくのがJavaScriptらしいかなと思いました。

元がJavaのコードなだけあり、最終形はクラス3つ、単体の関数はゼロという構成になっています。これはこれで良いですが、特定のオブジェクトに属さない関数が全然登場しないのは、JavaScriptっぽくないです。別の設計方針を考えてみるのも面白いかもしれません。

13
10
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
10