AtCoderやPaizaの問題をTDDでやる (TypeScript)

## 趣旨

競技プログラミング(AtCoder, Paiza 等)を TDD でやる環境を作ります。
ここでは言語として TypeScript を使う場合を説明しますが、どの言語であってもやることは同じです。

## 入力値と出力値をすばやく取得する

[![Image from Gyazo](https://i.gyazo.com/57de678d2847965663ae7a80ccfa1c09.png)](https://gyazo.com/57de678d2847965663ae7a80ccfa1c09)

こういうやつです。一つの問題に対して大体2〜3セット用意されています。
地道に一つずつコピーしてテストコードに貼り付けていっても良いのですが、こういう作業は一瞬で終わらせたいですね。

なのでこのようなスクリプトを用意します。

```js
// AtCoderの問題ページから入力値と出力値のJSONを取得するスクリプト
(function() {
  var selector = 'span.lang-en div.div-btn-copy + pre';
  var i = document.createElement('input');
  i.value = JSON.stringify(Array.from(document.querySelectorAll(selector)).map(e => e.innerText).reduce((acc, v, i) => { let j = Math.floor(i / 2); if (!acc[j]) acc[j] = []; acc[j].push(v.trim().replace(/\n/g, '\\n')); return acc }, []));
  document.body.append(i); document.querySelector('body > input:last-child').select();
  if (document.execCommand('copy')) { document.querySelector('body > input:last-child').remove(); alert('copied to clipboard.\n' + i.value); }
})();
```

これを例えば https://atcoder.jp/contests/abc086/tasks/arc089_a のような AtCoder の問題ページで実行すると、

[![Image from Gyazo](https://i.gyazo.com/9ad8a329c22d0652e9e785bbc056efc7.png)](https://gyazo.com/9ad8a329c22d0652e9e785bbc056efc7)

このようなアラートが表示され、

```json
[["2\\n3 1 2\\n6 1 1","Yes"],["1\\n2 100 100","No"],["2\\n5 1 1\\n100 1 1","No"]]
```

上記のような JSON が得られます。
この値はクリップボードにコピーされます。

セレクターの指定( `var selector = 'span.lang-en div.div-btn-copy + pre';` )はサイトによって変わりますので、自分が使っているサイト用に調整しましょう。

## テストコードに貼り付ける

そして、下記のようなテストコードの変数 `json` の部分に値を貼り付けます。

```js
describe('ABC086', () => {
  describe('C', () => {
    const json = `
    [["2\\n3 1 2\\n6 1 1","Yes"],["1\\n2 100 100","No"],["2\\n5 1 1\\n100 1 1","No"]]
    `;
    const params = JSON.parse(json);
    params.forEach(([input, output], i) => {
      it(`Test params[${i}]`, () => {
        expect(solve(input.split('\n'))).toBe(output); // solve はインポートしている。
      });
    });
  });
});
```

変数 `params` が **入力値と出力値の文字列を格納した要素数2の配列を格納する要素数nの配列** となるようにパースします。
JavaScript なら `JSON.parse()` とするだけです。

また、`solve()` は引数として `string[]` を受け取り、戻り値として `string` を返す関数です。
どの言語を使う場合でも、関数 `solve()` の引数と戻り値の型は統一しておくのがコツです。

## 僕が使っている実際のコード (抜粋)

### テストコード

テストのライブラリは Jest を使っています。

```ts:abc-086.spec.ts
import solveABC086C from 'src/atcoder/abc/abc-086-c';

jest.setTimeout(1000 * 2);

describe('ABC086', () => {
  describe('D', () => { .... }); 

  describe('C', () => {
    const json = `
    [["2\\n3 1 2\\n6 1 1","Yes"],["1\\n2 100 100","No"],["2\\n5 1 1\\n100 1 1","No"]]
    `;
    const params = JSON.parse(json);
    const customParams = [];
    runTest(solveABC086C, params, customParams);
  });

  describe('B', () => { .... });
  describe('A', () => { .... }); 
});

function runTest(solver, params, customParams = []) {
  const test = param => {
    const [i, o] = param;
    expect(solver(i.split('\n'))).toBe(o);
  };
  params.forEach((p, index) => {
    it(`Test params[${index}]`, () => test(p));
  });
  customParams.forEach((p, index) => {
    it(`Test customParams[${index}]`, () => test(p));
  });
}
```

### 提出用コード

`solve()` だけ `export` してテストします。

```ts:abc-086-c.ts
export default solve;

// main
function main(lines) {
  console.log(solve(lines));
}

// solver
function solve(lines: string[]): string {
  const { N, destinations } = parseLines(lines);
  /* 省略 */
  return canArrive ? 'Yes' : 'No';
}

// helpers
function parseLines(lines: string[]) {
  const N = +lines[0];
  const destinations = lines.slice(1, 1 + N).map(line => line.split(' ').map(s => +s));
  const obj = { N, destinations };
  debug(obj);
  return obj;
}

// general helpers
function debug(message?: any, ...optionalParams: any[]) {
  if (!!process.env.LOCAL_DEBUG) console.log(message, ...optionalParams);
}

// reader
if (!process.env.LOCAL_DEBUG) {
  const { stdin: input, stdout: output } = process;
  input.resume();
  input.setEncoding('utf8');
  const lines = [];
  require('readline')
    .createInterface({ input, output })
    .on('line', line => lines.push(line))
    .on('close', () => main(lines));
}
```

あとは `export LOCAL_DEBUG=1 && npx jest atcoder/abc/abc-086.spec --watch` のようなコマンドをターミナルで実行しておけば、コードを保存する度にテストが実行されます。

AtCoder なら TypeScript のコードをそのまま提出できますが、Paiza のような JavaScript のみ受け付けるようなサイトの場合は、トランスパイルした JS のファイルを提出することになります。
そのため、テストの watch をしているターミナルとは別に、トランスパイルを watch するターミナルも必要です。

ちなみに実行環境は下記のようです。(2019年5月13日調べ)

- AtCoder ... `{ node: 'v5.12.0', npm: '3.8.6' }`
- Paiza ... `{ node: 'v10.13.0', npm: '6.4.1' }`

それでは良い TDD ライフを。