Edited at

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


趣旨

競技プログラミング(AtCoder, Paiza 等)を TDD でやる環境を作ります。

ここでは言語として TypeScript を使う場合を説明しますが、どの言語であってもやることは同じです。


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

Image from Gyazo

こういうやつです。一つの問題に対して大体2〜3セット用意されています。

地道に一つずつコピーしてテストコードに貼り付けていっても良いのですが、こういう作業は一瞬で終わらせたいですね。

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

// 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

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

[["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 の部分に値を貼り付けます。

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 を使っています。


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 してテストします。


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 ライフを。