がんばるぞ

がんばります

買ってよかったもの2020

今年は色々と物を買った気がするので、買ってよかったものを振り返ってまとめようと思います。 トゥギャッターではリモートワーク手当として毎月3万円が支給される(!!!)ので、自宅の作業環境の改善による生産性の向上を口実に色々と利用させていただきました。(手当の使い方は自由なのだが、性格上気を抜くと貯金ばかりしてしまいそうだったので)

布団クリーナー

妻がハウスダストだかほこりだかのアレルギーなので購入しました。 実際に使ってみるとすごい。めっちゃホコリがとれる。 こんなホコリだらけの布団にくるまって寝てたんかい......って落ち込むくらいよく取れる。

僕は寝起きにくしゃみをよくしてしまうのだが、こいつを使うようにしてからくしゃみが激減した。 「寝起きはくしゃみが出るもの」程度に考えていたので、ホコリなどが原因だったことに初めて気がついた。妻さん、布団クリーナーを欲してくれてありがとう。

整腸薬

ヤクルトBL整腸薬 36包 [指定医薬部外品]

ヤクルトBL整腸薬 36包 [指定医薬部外品]

  • メディア: ヘルスケア&ケア用品

なんか妻が飲んでたので僕も試しに飲んでみたらアラ不思議。快便になる。 僕は基本便秘気味なのでたまに飲むようにするとこりゃあいいという感じ。リピ決定!!!!妻ありがとう。

冷凍餃子

妻にそそのかされるがまま買ってみると、ウマイっ!!!!!! 特に餃子鍋にすると良かった。 ご飯作るのダリ〜って時にとても便利なので、「俺のバックには大量の冷凍餃子がついてるから、いつご飯作るのダリ〜って思っても良いんだぜ」という心持ちになれるため日々のストレスが減った。 妻と冷凍餃子ありがとう。

カチューシャ

カチューシャはすごい!!!!!!!!!!!!!!!!!!!!いくら髪の毛が伸びても邪魔じゃない!!!!!!!!!!!!!!!!!!!! コロナで人と会うことも減ったし、髪の毛切るのめんどっち〜〜〜〜ってなっていたのでとても助かりました。

ちなみにこれは締め付けがきつい時はグイ〜〜〜て広げると広がってくれるところが良いです

断熱タンブラー

こいつにお湯/冷水を入れるとしっかり保温される。 朝に入れたコーヒーがお昼になっても暖かいままで感動した。 僕は基本的に水分をあまり摂取しない(というか忘れる)ので、仕事中などに「あっ、飲み物の存在忘れてた」となったときにこの保温力があるととても嬉しい。

サーキュレーター

これは衝撃の事実なのですが、僕の作業部屋にはエアコンがない。取り付けることができない。 コロナでリモートワーク化してから、この事実がシャレにならなくなったので藁にもすがる思いで購入。 隣の部屋の空気を作業部屋に送ることでなんとかなった! サーキュレーターありがとう!!!引っ越さなくて済んだ!!!

ウルトラワイドディスプレイ

トゥギャッターのリモート手当を2ヶ月ため込んで購入した。 こいつは94Wの給電もついてるのでMBPの充電ができる! さらにUSBポートもType-Cが1つとUSB3が2つ付いてるので、配線がめっちゃスッキリするという優れもの! 画面がでかいは正義!

Oura Ring

トゥギャッターのリモートワーク手当があまりに最高すぎるので、手当を有効活用して会社に貢献しなくてはと思い調べていたところ発見。 睡眠の質が上がれば生産性も上がるはず!!!!ということで睡眠トラッキングを始めました。 トラッキングをしてみたところ想像以上に睡眠の質がよくないことが発覚したため、諸々がんばって睡眠の質を改善した結果、集中力が以前より向上している...というか安定することが実感できました。

Oura Ringはいいぞ!

セパレートキーボード

肩こりが辛かったため購入。 普通のキーボードと比べて使用感がそこまで変わらず、肩こりが軽減されたのでかなり満足! 左右のキーボードをつなげるケーブルが少し短めなのがちょっと不満!

バロンチェア

腰痛になりつつあったのでオフィスバスターで2万程度で購入。安い!ありがとう!!!!!! 腰がめっっっっっっっっちゃくちゃ楽になったので高い椅子はすごい!!!!!ってなりました。

いかがでしたか?

いかがでしたか?

コンピュータシステムの理論と実装 Chap3

www.amazon.co.jp

これの三章です。

三章の概要

この章では順序回路について学び、フリップフロップレジスタ、メモリなどを実装する。

順序回路

順序回路とは時間に依存した回路のこと。
1章、2章で実装した回路は時間に依存しておらず、組み合わせ回路と呼ばれる。

クロック

時間の経過をモデル化したやつ
こいつが各回路に信号(タイムユニット)を送り続けることによって、時間に依存した回路を実装することができる。

フリップフロップ

順序回路の中で最も基本的な回路。
いくつかある種類のうち、本書では D型フリップフロップ(DFF) を採用している。
こいつのおかげで回路の同期的な計算が可能となる

DFF は 1bit の入力とクロック入力という2つの入力と1bitの出力があり、振る舞いとしては一つ前のタイムユニットの入力を出力するだけの回路である
この回路を元にさまざまな記憶素子を実装していく

レジスタ

データを格納したり読み出したりすることができる記憶装置
多ビットを記憶できるレジスタが持つ値はワードと呼ばれる。

メモリ(RAM)

任意の長さのワードを記憶できる装置。
物理的な記憶位置に関係なく、同じ時間で直接値にアクセスできる

カウンタ

タイムユニットが進むごとに任意の整数が加算されていく回路。
任意の整数は 1 が用いられることが多い。

順序回路の実装

2章までに実装した回路と予め用意されているフリップフロップ回路を用いて、レジスタ・メモリ・カウンタなどの順路回路を実装する。

以降は僕が実装したコードが出てくるので、ネタバレされたくない人は読まない方がよいです

1bitレジスタ

in と 制御ビットの load の2つを入力として受け取り、 load が1の場合に保存された値を読み込み、 load が 0 の場合に in の値を書き込めばOK

Mux(a=loop, b=in, sel=load, out=mux);
DFF(in=mux, out=out, out=loop);

16bitレジスタ

1bitレジスタの16bit版。
なので、1bitレジスタを16個並べるだけで良さそうだったのでそうした

Bit(in=in[0], out=out[0], load=load);
...
Bit(in=in[15], out=out[15], load=load);

RAM(8|64|512|4K|16K)

inload に加えて address の3つの入力を受け取り、 address の値によって読み書きするレジスタを選択出来ればOK。

各16bitレジスタの出力を Multiplexor で振り分ければいけそうだったのでそうした。 また、指定されたレジスタ以外に書き込みが発生するとまずいので、 入力の loadDemultiplexor で振り分けてる

以下のコードは RAM8 だけど、それ以外のRAMも同じようなロジックでいける。

DMux8Way(in=load, sel=address, a=load1, b=load2, c=load3, d=load4, e=load5, f=load6, g=load7, h=load8);
Register(in=in, out=out1, load=load1);
Register(in=in, out=out2, load=load2);
Register(in=in, out=out3, load=load3);
Register(in=in, out=out4, load=load4);
Register(in=in, out=out5, load=load5);
Register(in=in, out=out6, load=load6);
Register(in=in, out=out7, load=load7);
Register(in=in, out=out8, load=load8);
Mux8Way16(a=out1, b=out2, c=out3, d=out4, e=out5, f=out6, g=out7, h=out8, sel=address, out=out);

カウンタ

in と制御ビットである inc , load , reset の4つの入力を受け取り、 制御ビットに合わせて動作を変えればOK
inc が 1 の場合は格納されている値をインクリメントし、 load が 1 の場合は格納されている値を in で上書きし、 reset が 1 の場合は格納している値を 0 にする。
値の格納にはレジスタを使う

ALU と同じような実装で良さそうだったのでそうした。

// inc
Inc16(in=loop, out=incremented);
Mux16(a=loop, b=incremented, sel=inc, out=in1);

// load
Mux16(a=in1, b=in, sel=load, out=in2);

// reset
Mux16(a=in2, b[0..15]=false, sel=reset, out=in3);

Register(in=in3, load=true, out=out, out=loop);

感想

うお〜メモリを実装したぞ〜〜

コンピュータシステムの理論と実装 Chap2

www.amazon.co.jp

前回に引き続きこれです

2章概要

2進数、符号付き2進数、2の補数、ブール算術など理解し、加算器・算術論理演算機を実装する

2進数

0と1のみを使って数を表現するやつ

符号付き2進数

正の数と負の数を表現できる2進数

2の補数

符号付き2進数を実現するために使われているやつ。
最上位bitの状態で正か負かを判断できる。
補数を求めるための数式は 2**n - x ※ n= bit数

たとえば 8bitで -5 を表す場合は 2**8 - 5 = 256 -5 = 251 となり、2進数に直すと 11111011 となる
これに8bitの5である 00000101 を加算してみると 00000000 となり (繰り上がった9bit目はオーバーフローで無視) 答えが0になっているので補数であることが確認できる。

符号付き2進数において2の補数での表現を採用するメリットは、減算を加算と同じ手順で計算できるためシンプルな実装になることらしい。
たとえば4bitの 3-2 であれば 0011 + 1110 となり、計算結果は 0001 (繰り上がった5bit目は無視) なので、正しく計算できていることがわかる。

加算器とALUの実装

1章で実装した論理ゲートを用いて、いくつかの加算器と算術論理演算機(ALU=Arithmetic Logical Unit)を実装する。

以降は僕が実装したコードが出てくるので、ネタバレされたくない人は読まない方がよいです

半加算器

2つの入力の和を出力すればOK。
出力パターンは 00, 01, 10 の3つ

最下位ビットは2つの入力がそれぞれ 0 と 1 だった場合にのみ 1 を出力し、それ以外は 0 を返せばよく、これは Xor の挙動と同じなので Xor を使い、
最上位ビット(=繰り上がり) は2つの入力が両方とも 1 だった場合にのみ 1 を出力すればいいので And を使った。

Xor(a=a, b=b, out=sum);
And(a=a, b=b, out=carry);

全加算器

3つの入力の和を出力すればOK。
出力パターンは 00, 01, 10, 11 の4つ

半加算器を2つ使用して最下位bitの和を求めて、どちらかで繰り上がりが発生していた場合に最上位bitに1を出力すれば良さそうと思ったのでそうした

HalfAdder(a=a, b=b, sum=tmp, carry=carry1);
HalfAdder(a=c, b=tmp, sum=sum, carry=carry2);
Or(a=carry1, b=carry2, out=carry);

16bit加算器

2つの16bitの入力の和を出力すればOK。
オーバーフローは検出しない

半加算器と全加算器を並べるだけで良さそうだったのでそうした

HalfAdder(a=a[0], b=b[0], sum=out[0], carry=carry1);
FullAdder(a=a[1], b=b[1], c=carry1, sum=out[1], carry=carry2);
...
FullAdder(a=a[14], b=b[14], c=carry14, sum=out[14], carry=carry15);
FullAdder(a=a[15], b=b[15], c=carry15, sum=out[15]);

インクリメンタ

与えられた16bitの入力に1を加算すればOK

先ほどの16bit加算器を使うと楽そうだったのでそうした

Add16(a=in, b[0]=true, b[1..15]=false, out=out);

ALU

2つの16bitの入力といくつかの制御ビットをもとに計算する。
出力は計算結果の out , out が 0 であるかどうかを表す zr , outが負の数であるかどうかを表す ng の3つ。

制御ビット 説明
zx 入力 x をゼロにする
nx 入力 x を反転する
zy 入力 y をゼロにする
ny 入力 y を反転する
f True であれば加算、 False であれば And演算 する
no 出力を反転する

If文使いてえ〜と思いながら Multiplexor を使ってそれぞれの制御ビットを適用した計算結果を選択していく感じで実装した。

out は制御ビットを適用したあとの数値をそのまま出力し、
zr は out の全てのbit が 0 だった場合に 1 を出力すればよいため Or8Way (8bitの中に1つでも 1 があれば 1 を出力する論理ゲート) を2つ使い
ng は最上位ビットが 1 であれば 1 を出力し、そうでなければ 0 を出力すればいいため、 out の15bit目をそのまま出力した

変数名が無になって辛い...。

// zx, nx
Mux16(a=x, b[0..15]=false, sel=zx, out=x1);
Not16(in=x1, out=x2);
Mux16(a=x1, b=x2, sel=nx, out=x3);

// zy, ny
Mux16(a=y, b[0..15]=false, sel=zy, out=y1);
Not16(in=y1, out=y2);
Mux16(a=y1, b=y2, sel=ny, out=y3);

// f
Add16(a=x3, b=y3, out=xAddY);
And16(a=x3, b=y3, out=xAndY);
Mux16(a=xAndY, b=xAddY, sel=f, out=f1);

// no
Not16(in=f1, out=notF1);
Mux16(a=f1, b=notF1, sel=no, out=out, out[0..7]=out0to7, out[8..15]=out8to15, out[15]=ng);

Or8Way(in=out0to7, out=zr1);
Or8Way(in=out8to15, out=zr2);
Or(a=zr1, b=zr2, out=zr3);
Not(in=zr3, out=zr);

感想

ALUはもうちょっと良い実装があるんだろうなーと思ったけど特に浮かばなかったのが残念。
模範解答的なやつがあれば見てみたいなー

コンピュータシステムの理論と実装 Chap1

www.amazon.co.jp

最近この本をやり始めたのでメモ

1章概要

ブール理論を理解し、本書から提供されているハードウェアシミュレーターを用いて数種類の論理ゲートを実装する。

論理ゲート

ブール関数を実装した物理デバイスのことらしい。が、実際に物理デバイスを作る必要はなく、ハードウェアシミュレーター上で論理ゲートを実装すればOK。

ブール関数とは And とか Or とか Not のように、 n個の入力値を元に新しいブール値を出力するような関数のこと

And だったら2つの入力値が共に True であれば True を出力し、それ以外であれば False を出力するし、 Not であれば1つの入力値を反転させた出力をする。
出力値は1つとは限らず、例えば Demultiplexor と呼ばれるブール関数は複数の出力を持つ

論理ゲートの実装

Nand (=not and) という論理ゲートだけが予め用意されているので、 Nand を使って他の論理ゲートを実装していく。 実行効率についてはあまり考えずに、最初に浮かんだロジックをとりあえず実装して進める方針でやっていく。

以降は僕が実装したコードが出てくるので、ネタバレされたくない人は読まない方がよいです

Not

与えられた入力値を反転して出力すればいい。

Nandしか使えないので以下のような実装になった。

Nand(a=in, b=true, out=out);

PHPだとこんな感じになる

<?php
function not(bool $in): bool
{
    return nand($in, true);
}

And

入力値が2つとも True だったら True を出力し、それ以外であれば False を出力すればいい。

そもそも Nand!And なので、さらに否定すれば !!And となって And になるじゃんと思ったので以下のような実装になった。

Nand(a=a, b=b, out=nand);
Not(in=nand, out=out);

PHPだとこう

<?php
function and(bool $a, bool $b): bool
{
    return not(nand($a, $b));
}

Or

2つの入力値がどちらも False である場合に False を出力し、それ以外の場合は True を出力する。

Nand は 2つの入力値が True である場合のみ True を出力し、それ以外の場合は False を出力するという Or の動作と似てる感じだったので、入力値をそれぞれ否定すればいいんじゃねということで以下のような実装になった

Not(in=a, out=notA);
Not(in=b, out=notB);
Nand(a=notA, b=notB, out=out);

Xor

2つの入力値がそれぞれ異なる値の場合に True を出力し、そうでない場合に False を出力する。

OrNand がそれぞれ True を出力すれば2つの入力値が異なる状態であると言えると思うので、以下のような実装になった

Nand(a=a, b=b, out=nand);
Or(a=a, b=b, out=or);
And(a=nand, b=or, out=out);

Multiplexor

a,b,selという3つの入力値を受け取り、selが False であれば a を selが True であれば b を出力すればOK。

こういった分岐はif文を使うことが前提な思考になっていたので、少し戸惑ってしまった。
あと変数名をどうすればいいかわからなさすぎる

b の値を出力する場合は sel と b を And 関数に通せばいいが a を出力する場合はそうもいかない。
が、sel を反転すれば a も b と同じ扱いが出来るのではという感じで以下の実装になった。

Not(in=sel, out=notSel);
And(a=a, b=notSel, out=a1);
And(a=b, b=sel, out=b1);
Or(a=a1, b=b1, out=out);

Demultiplexor

in,sel という2つの入力値をとり、a, b という2つの出力をする。 sel が False であれば a=in, b=0 を sel が True であれば a=0, b=in を出力すればOK。

先ほどの Multiplexor を使えば簡単そうだったのでそうした

Mux(a=in, b=false, sel=sel, out=a);
Mux(a=false, b=in, sel=sel, out=b);

多ビットAnd, Not, Or, Multiplexor / 複数入力 Or, Multiplexor, Demultiplexor

他にも課題のゲートはいくつかあったが、新しい考え方は特になかったのでスキップ

感想

ブール関数だけで Multiplexor みたいなゲートを実装するのはすごく新鮮で面白かったなー

2019年まとめ

主な出来事

(やっぱり技術的な挑戦はあまりしてないな)

今年買った本

良かった本だけ掲載。

ザ・ファブル」と「悪魔のメムメムちゃん」以外の漫画は全てカバレッジを上げている期間に読みました。精神を病まないようにすることに必死でした。

うーん、技術書を全く読んでない。これはひどい

あっ、大事な本を忘れてました。
僕も寄稿させていただいたみんなのPHPは最高なので買ってください!

2019年の雑感

2019年、色々ありました。

技術的に大きく進歩した実感はあまりないけど(遊びすぎた)人間関係がすごく充実した良い一年だったと思います。

知り合いが増えた

一番の変化はやはり知り合いが増えたことでしょうか。
hamacoさんのおかげもあり本当に知り合いが増えた。死ぬほど増えた。
去年は雲の上だと思っていたような人ともTwitter上で会話が出来るようにもなりました。
本当に信じられない変化ですね。

心を許せるレベルの知り合いも複数人出来ました。
すごい幸運。ラッキーボーイ。みなさんいつもありがとうございます。

そして、勉強会などで会ったことのない人から「Twitterで顔だけ見たことあります」みたいなことを言われるようになった。
どうかしてる。

コミュニティは本当に大事だなと実感した1年だったので、来年も無理のない範囲でコミュニティに貢献できたらなと思ってます。

みんなのPHPに寄稿した

商業誌ってずっと目標にしてたんですよね。まさかこんなに早く関わることが出来るとは。 みんなのPHP、もっと売れるといいな〜〜!

次は単著出せるようにがんばるぞ〜。

遊びすぎた

本当に遊びすぎた。 仕事でカバレッジを20%から90%に上げたりはしたけど、やれば誰にでも出来ることだし、何にも挑戦してなくてヤバい。

未熟さを思い知らされた

すごい人ってすごくて(語彙力)、会話をするたびにレベルの違いを思い知らされます。

中でも印象に残っているのはDDD Meetupの懇親会の時のことで、すごい人たちが繰り広げているモデリングの議論にまっっっっっったくついていけなかったことです。

レベルが高すぎるゆえに質問することすら出来なくて、とてつもない悔しさと「この人たちスゲェ」という興奮が綯い交ぜになった感情を抱きました。

このように、すごい人たちに囲まれていると「このままじゃヤバい」という感情がムクムクと湧いてくるので、来年はネタキャラとしてだけではなく、Webエンジニアとしてもみんなと会話が出来るようにならないとなーと思っています。
とくに今年はみんなと知り合えた嬉しさからか遊びすぎた(はしゃぎすぎた)ので、来年は遊びはほどほどにしてたくさん勉強しようと思います。

カバレッジを上げて燃え尽きてる場合じゃない。

来年の抱負

  • Laravel JP Conference 2020やるぞ!
  • 設計勉強するぞ!
  • 会社のサービスを伸ばすぞ!
  • フロントエンド方面も勉強するぞ!
  • 個人でWebサービスリリースするぞ!
  • 勉強するぞ!
  • 勉強するぞ!
  • 英語勉強するぞ!
  • 音楽もやるぞ!
  • 勉強するぞ!
  • 実装速度を早くするぞ!

やるぞ〜〜〜〜〜〜〜!!!!!!!!!

*1:僕よりも前から焼肉の人

*2:由緒正しい焼肉の人

*3:リファクタリングコストの都合上、不本意な(中途半端な)実装で妥協せざるを得なかったクラス

PHP Conference Japan 2019で登壇してきました

かなり時間が経ってしまいましたが、PHP Conference Japan 2019 にて登壇してきました。

togetter.com

20%もなかったカバレッジを90%まであげたので、そのためにしたことの話をしました。

動画はそのうちYoutubeに上がるのかな?上がったらこの記事に貼っておきます。 動画あげてもらいました! www.youtube.com

この記事は登壇後記という体のただの乱文です。

ポエム

いやぁ、普通に疲れました。
登壇が終わってから、ブログを書いてる今この瞬間も完全に燃え尽きてます。

一つのエンドポイントにテストを追加するとだいたい0.04%〜0.08%くらいはカバレッジが上がってくれるんですが、 途中 0.01%〜0.03%しかカバレッジが上がらないみたいなことが連発していた時はだいぶ精神的に追い詰められました。

けどなんとか登壇までに90%まで上げることが出来てよかった。

カバレッジをあげたかった理由は登壇の最初でも話したんですが、それ以外にも 「テストがあれば防げたようなバグを僕がデプロイしてしまったから」ってのがあるんですよね。

C0レベルのテストでも防げたようなしょうもないバグで、でもめちゃくちゃ重要な機能にバグを放り込んでしまって めっちゃくちゃ情けなかったんです。

この情けないミスの償いと、同じミスを僕以外のエンジニアにさせないためにカバレッジを上げなければと強めに決心しました。

コツコツと自分の関わった範囲にテストを追加していくような手段も取れたんですけど、 派手にカバレッジを上げ、それをネタに登壇することで広報的な効果も追加出来るんじゃないかなーと思って、一気に90%まで上げることにしました。

ほら、テストってただでさえユーザーに対して直接価値を届けられるようなものではないじゃないですか。(主観)
なので、テストに大きくリソースを割くことに抵抗というか葛藤があったんですが、広報的な効果を追加すれば会社的なメリットも増えるし、まぁいいんじゃないかなーと。

ですが実際に登壇した結果、広報的な役割をしっかり果たせたのかというと

  • ポジティヴな印象を与えることが出来たのかわからん
  • PHP界隈の話題にすることが出来なかった

という点でかなり微妙なところではあるので、もうちょっと技術的に面白い話が出来たらよかったかなぁという反省があります。

カバレッジを上げて得たもの

チームメンバーへの感謝の心

僕がテストを書くことだけに集中出来るようにチームメンバーのみなさんがだいぶ気を使ってくれたので、本当に感謝しています。
具体的には、ほぼフルリモートで働くことを許可してくれたり、なるべく僕にテスト以外の作業が発生しないように配慮してくれたりですね。
みんなの配慮がなければ絶対に登壇までに90%は間に合わなかったと思います。

この場を借りてお礼を言います。ありがとうございました。

デプロイの安心感

C0レベルのテストは90%の箇所に書かれているので、とりあえず正常系であればテストが通っていればほぼほぼ動くのではって感じです。
ただ、実装をみて仕様を想像しながらテストを書いてるだけなので、漏れてるケースはちょこちょこありそうだなー。

カバレッジを追い求めることの無意味さ

おい!!!!!!!って感じなんですけど、カバレッジを追いかけるのはマジで虚無だなという感想を得ることが出来ました。

あ、いや、今回カバレッジを上げたが無意味だったという話ではないです。念の為。
今回カバレッジを上げたことは大きな意味がありました。が、カバレッジをKPIにする必要はないなという話です。

登壇の最中にも軽く話したんですが、弊社のサービスの場合は75%や80%を超えたあたりでだいたい必要そうな箇所にはテストをかけたなーという感覚があって、 それ以降は登壇のタイトルで90%って言ってしまったから仕方ねぇって感じで、「書かなくてもいいかなー」って感じのユニットテストをもりもり追加するような感じになっていました。

おそらくサービスごとに適切なカバレッジがあって、それは必要な箇所にテストを書いた結果自然と導き出されるものなので、カバレッジをKPIにしてテストを書いていくと不必要なテストが発生するんじゃないかなと思います。

みんなカバレッジとか気にせずにちゃんとテスト書いていこうな。

とはいえ、ローカルのPHPを7.4にあげてみた時にバグを発見してくれたのは「このテスト書かなくてもいいんじゃないかなー」って思って書いたユニットテストだったりしたので「このテストは必要ない」って判断するのめっちゃむずいよねって感じなんですが・・・。

また別の話ですが、テストは最終的には負債でしかないっていう意見もあって、僕も割と共感してます。

PHPは動的型付言語だし型の制約が強いわけではないしって理由でテストを書かないという選択をするのはだいぶ難しいと思うんですが、テストを書かなくて良いようなコードをかけたらよいなぁと思ってます。

PHPStanやPhanという静的解析がだいぶ強くなっているので部分的には可能かなぁ、どうだろう。

テストの責務についての考え方

Unitテストや機能テスト、受け入れテストでそれぞれのテストが担保すべき内容って違うんですよね。

機能テストで実行されているコードはUnitテストが必要ないのかというと絶対にそんなことはなくて、 テストの関心ごとが違うからそれぞれテストしないといけないと思うわけです。
機能テストの関心ごとじゃないところまで機能テストで担保しようとするとテストが大きくなりすぎちゃいますしね。

漠然と今までも理解していたんですが、今回死ぬほどテストを書きまくった結果そこらへんの考えがより明確に(深く?)なったので、どこかで改めてまとめたいなーと思ってます。

おわりに

はい、こんな感じかなー

テストは書いたので、次はこのテストたちを武器にリファクタリングに励みたいと思います。

おわり

PHPカンファレンス沖縄2019のLTでミューテーションテストについて話してきました

PHPカンファレンス沖縄2019でLTをしてきました。

togetter.com

speakerdeck.com

LTの内容はミューテーションテストの基礎的な概要を紹介するといったものだったのですが、5分という時間の制約上伝えられなかったことがたくさんあるので、この記事で補足を兼ねて解説しなおしたいと思います。

※この記事はあくまで僕の解釈です。一般的な解釈と異なる記述がある可能性があります。

ミューテーションテストとは

ミューテーションテストとは、我々が実装したアプリケーションコードの一部を書き換え、書き換えたコードに対してテストを再度実行することでテストの品質を測ろうというものです。

簡単に表現すると「アプリケーションコードに変更が加わったのに、既存のテストがパスするのはおかしいんじゃないか?」という視点で品質を評価するということだと思います。要するにテストコードに対するテストですね。

ミューテーションテストが必要になる理由

一般的なテストコードは、我々が実装したアプリケーションコードに対して実行し、アプリケーションの品質を保証する(したい)ものだと思います。

これはとても重要なもので、テストコードを書くことによって

  • アプリケーションの品質の維持
  • 安全なリファクタリングの実現
  • 実装時に見逃していたバグの発見
  • etc

など、いくつかのポジティブな効果が期待できます。 *1

一方でそれらの効果を期待するためには「品質の良いテストコードである」ということが求められるかと思います。しかし、テストコードの品質を数値化するということは非常に難しい課題です。

広く知られる指標としてコードカバレッジがありますが、これはテストでどれだけのアプリケーションコードが実行されたかということを示すだけで、テストコードの品質についてフィードバックを得られるわけではありません。*2

そこで役に立つかもしれないのがミューテーションテストと呼ばれるものです。

どうやっているのか

先に述べたように、アプリケーションコードを変異(ミューテート)させ、それに対して再度テストを実行することでテストの品質を評価します。

どのような変異を起こしてくれるのかというと、これは各テスティングフレームワークによって異なると思います。

PHPにはinfectionというテスティングフレームワークがあり、公式ドキュメントで様々なミューテーター(変異の種類)が紹介されています。

ミューテーターはかなり多く用意されているので、少しだけ紹介すると

説明 元コード 変異後
配列に対する操作の変異 $a = array_filter(['A', 1, 'C'], 'is_int') $a = ['A', 1, 'C']
論理式の変異 $a < $b $a <= $b
論理式の変異 $a < $b $a >= $b
Loopの制御文の変異 break continue
例外処理の変異 throw new Exeption new Exception
例外処理の変異 try {} catch (Exception $e) {} finally {} try {} catch (Exception $e) {}

など、様々な変異を起こしてくれます

例えば、以下のようなコードがあったとします。

<?php
class Number
{
    private $value;

    public function __construct(int $value)
    {
        $this->value = $value;
    }

    public function isSmallerThan(int $value): bool
    {
        return $this->value < $value;
    }
}

isSmallerThan というメソッドは、与えられた数値よりも $this->value の値の方が小さければtrue、そうでなければfalseを返すという簡単なメソッドです。

そして、このコードに対するテストコードが以下です。

<?php
use PHPUnit\Framework\TestCase;

final class NumberTest extends TestCase
{
    public function testIsSmallerThan()
    {
        $number = new Number(15);
        $this->assertTrue($number->isSmallerThan(20));
    }
}

15のNumberの isSmallerThan に20の数値を渡しています。
もちろん15は20より小さいのでtrueを返し、テストはパスします。

この状態でinfectionを使いミューテーションテストを実行してみると以下のような実行結果が出力されます。

--- Original
+++ New
@@ @@
     }
     public function isSmallerThan(int $value) : bool
     {
-        return $this->value < $value;
+        return $this->value <= $value;
     }
 }

この例では <<= に変異させても既存のテストが全てパスしてしまうこと、つまり境界値テストが足りていないことがわかり、(ミューテーションテストから見た)テストの品質をあげる具体的なヒントを手に入れることができました。

このようにして、既存のテストに足りていないと思われるテストをミューテーションテストを通して知ることができるようになります。

課題

ここまでの話でミューテーションテストに対してかなり期待を持った方が多いのではないかと思います(多いといいな)

しかし、ミューテーションテストを実際に運用するとなるといくつか課題が浮かびます。

実行時間

ミューテーションテストは、かなり重い処理になります。 アプリケーションコードが多くなればなるほど多くの時間が必要になり、公式サイトにもアプリケーションの規模によっては数時間かかるかもしれないというような文言があります。

If you have thousands of files and too many tests, running Mutation Testing can take hours for your project.

これに対するアイデアとしては、masterブランチやdevelopブランチと差分のあるファイルにのみinfectionを実行するというものです。

$ infection --threads=4 --filter=$(git diff origin/master --diff-filter=AM --name-only | grep src/ | paste -sd "," -) --ignore-msi-with-no-mutations

この方法であればPRが大きくならない限りは現実的な実行時間でCI上でもミューテーションテストを実行することが可能になると思います。

ミューテーションテストの全ての指摘に対応するのはコストが高い

デフォルトの設定でinfectionを実行してみると、ちょっとしたコードでもなかなかの量の指摘が発生すると思います。

中には有益な指摘も当然ありますが、全てに対応するのはコストの割にはメリットが少なさそうだなというのが僕の感想です。

これに対する有効そうな手段はいくつかあると思います。

min-msiを調整する

msiは Mutation Score Indicatorの略で、ミューテーションテストのスコアです。 MSIは0%から100%までの値を取り、数値が大きければ大きいほどミューテーションテスト的に品質の良いテストということになります。

開発チームで許容出来る最低スコアを設定することで、全ての指摘事項に対応しなくてもミューテーションテストをパスすることが出来るようになります。

ですが当然全ての指摘の重要度が等しいわけではないので、有益な指摘を見逃している場合であってもmin-msiを超えているテストであればミューテーションテストをパスすることになるので注意が必要です。

不要なミューテーターを無効にする

infectionではミューテーターを細かく設定することができます。(https://infection.github.io/guide/profiles.html)

必要と思われるミューテーターのみ有効にすることで、ミューテーションテストの対応コストを調整することも可能です。 が、こちらも有益な指摘を見逃す可能性が発生することを念頭に置く必要があると思います。

おわりに

最近ミューテーションテストの存在を知り、個人で開発しているライブラリに対してinfectionを導入してみましたが、まだまだ検証が足りていません。 どこまでテストの品質が上がるのか、そしてどれだけ有効なのか未知数なので、もうちょっとしっかり触ってみたいなーというところです。 実際にミューテーションテストをゴリゴリ使っていらっしゃる方がいれば是非吉田あひるまでご連絡ください!

*1:テストコードの内容によってはむしろ技術的負債になることもありえますが、テーマから逸れるため割愛します

*2:もちろんテストされていないアプリケーションコードを把握できるというのは大きなメリットです。