自分のサービスにモブプログラミングでリグレッションテストを導入している話

この内容は [ソフトウェアテスト Advent Calendar 2020](https://qiita.com/advent-calendar/2020/softwaretesting)の6日目の記事です

#変更履歴
2021/1/24 6回目から8回目までを追記

#自分のサービスにリグレッションテストを書いていなかった
僕は趣味でマンガログサービス「[マンガ読んだ！！](https://manga-yonda.com/)」を作っています。このサービスは規模が小さいこともあり特にリグレッションテストを書いていませんでした。そのため本番環境にデプロイした後に特定のページがエラーになっていたこともしばしばありました。とはいえ他にもやることはあったので、テストは入れたい入れたいとは思いつつ、ずっと入れていない状態でした。

#SPAでURLの存在チェックするテストは意味がない
で、ある時、またページがエラーになっていたので、せめてHttpClientを使ってURLが生きているかのチェックをする簡単なテストを作れないかと思いました。しかし、マンガ読んだ！！はSingle Page Application(以下SPA)です。SPAでステータスコードが成功かのテストをしても（作り方にもよりますが）、失敗がソフト404エラーになるため全部成功か全部失敗の2択にしかなりません。SPAでテストしたければ、Seleniumなどのフレームワークで人間の操作を自動化してテストしてやる必要があり、ちょっとハードルが高めです。

```C#
//このテストは全部成功か全部失敗しかない。。。
[TestMethod]
public async Task UrlStatusTest()
{
    using var client = new HttpClient();
    (await client.GetAsync(@"https://manda-yonda.com")).IsSuccessStatusCode.IsTrue();
    (await client.GetAsync(@"https://manda-yonda.com/mangas")).IsSuccessStatusCode.IsTrue();
    (await client.GetAsync(@"https://manda-yonda.com/series")).IsSuccessStatusCode.IsTrue();
    (await client.GetAsync(@"https://manda-yonda.com/author")).IsSuccessStatusCode.IsTrue();
}
```
#モブプロによるリグレッションテストの導入
そこで、ちょっと思いついて、自分よりテストとSeleinumの知識がありそうな人とモブプロ的にリグレッションテストが導入が出来たら面白いかなと思って、知人に声をかけてみました。この試みは予想以上に面白く、また成果が出ました。今日の時点で[マンガ読んだ！！zoomガヤガヤ会](https://manga-yonda.connpass.com/)と称して既に5回やっていますが、かなりリグレッションテストが機能してきています。そこで、ここまでの経緯をダイジェストで書いていこうと思います。

#1回目のモブプロ
で、最初のモブプロです。Visual Studio Live Shareが上手く使えなかったので、一旦ドライバーは僕1人だけにしました。他の人は口を出します。ポイントとして出たのが**待つ**対象を何にするかという話でした。つまりテストでページ移動をする場合に、何がどうなったらページが移動したと判断するか。具体的にはプロダクトコードでページが切り替わったタイミングで待ち用のidなりclassなりを作って、そこに状態を入れる必要があります。

```html
<div class="wait" state="@State"> //@StateはSPAのフレームワークで変数を入れる書き方
```
ここで自分の中では疑問だったidとclassの使い分けについて聞いてみました。これに関しては明確な答えが返ってきました。対象が一意になるならid、そうでなければclass。で、今回作るものは誰が使うか？テストが使う。逆に言えばテスト以外は使わない。つまりid一択。なるほど！と思いました。次にidのvalueですが、data-stateを進められました。これがあるとjsでdataset.stateを使えば簡単に書き換えられる。なるほど知らなかったのでこれは便利だなと思いました。

```html
<div id="wait" data-state="@State"> 
```
これで、ページが変わって、DOMのレンダリングが終わった状態でstateに値を入れれば、それを待つ対象に出来ます。

```C#
DriverWait.Until(drv => drv.FindElement(By.Id("wait")).GetAttribute("data-state") == state);
```
後は待った上で、何をテストするかを決める必要があります。stateをそのまま対象にしても良いのですが、これは別にすることにしてタイトルをテストしました。

```C#
Assert.AreEqual(Driver.Title, "トップ");
```
当然プロダクトコードでは最初にtitleを書き換えて、最後にstateを書き換えます。これでSPAでトップページのタイトルをチェックするテストを書きました。大体2時間（雑談含む）ぐらい経ったのでここで終わりにしました。

#2回目のモブプロ
2回目はまずこのサービスで抑えるべきページはいくつあるのかという話になりました。全ページだと30万以上ありますが、ベースとなるページは8画面でこのテストが書ければ十分となりました。ここまでzoomで会話しながら何となく画面共有しているテキストエディタに適当な単語を書きながら話をしていました。そしたら今書いたメモを整理して日本語にすれば、それがテストだと言われました。なるほど。そういうアプローチがあるのかと感心してしまいました。清書したメソッド名が以下です。

```C#
public void トップ画面を表示()
public void トップ画面から移動してマンガ画面を表示()
public void トップ画面からマンガ画面に移動してマンガ詳細画面を表示()
public void トップ画面から移動してシリーズ画面を表示()
public void トップ画面からシリーズ画面に移動してシリーズ詳細画面を表示()
public void トップ画面から移動して作者画面を表示()
public void トップ画面から作者画面に移動して作者詳細画面を表示()
public void トップ画面からログイン画面に移動してログインしてメイン画面を表示してユーザー名をクリックしてユーザー画面に移動()
```
ここで1つ方針を決めました。一旦ダーティでも良いので、8画面のテストを作りきる。この方針は後々面白い効果を産みました。結局2回目は3個のテストを書いた所で終わりました。

#3回目のモブプロ
3回目のモブプロでは、前回ダーティでも良いことにしたので、Stateを0,1,2の数字で扱っていましたが、ここから3,4となっていくのはダーティというより煩雑なので、文字に変えました。

```html
<div id="wait" data-state="0"> <div id="wait" data-state="1"> 
↓
<div id="wait" data-state="top"> <div id="wait" data-state="manga"> 
```
それ以外は引き続きダーティルールで、作ったメソッドを**コピペ**しては必要な所だけ書き換えていきました。これに関しては僕を含め、皆、思う所はありつつ、スルーという状況でした。大分テストが揃ってきた時に、「流石にこの作り方罪悪感半端ない」と言ったら、みんな同じ気持ちだったので盛り上がりました。最終的には「まあこれが共通認識なら」となりました。もしテストの数がもっと多かったら流石にどこかで不満爆発していたかもしれませんが。テストは7/8書いて残り1個で終わりにしました。

#4回目のモブプロ
前回、詳細ページはタイトルが動的に決まるので、それをURLから取るようにしていたのですが、この方法には問題があるという話をしたら、むしろリグレッションテストテストならユニットテストとは違い、前のページでクリックした名前を変数に残してそれと比較するのがスジだろうとなりました。確かに流れをテストするならこれが正しい方法な気がしました。

さて、今回はログインを含んだ最後のテストを書きました。とはいえ、テキスト入力したりボタンクリックしてログインするのはSeleinumのお家芸なので、トライ＆エラーはありつつ実装していきました。ログイン出来て画面移動が終わったところで一旦テストを分けて、最後のアクションのテストも書きました。

```C#
public void トップ画面からログイン画面に移動してログインしてメイン画面を表示してユーザー名をクリックしてユーザー画面に移動()
↓
public void トップ画面からログイン画面に移動してログインしてメイン画面を表示()
public void トップ画面からログイン画面に移動してログインしてメイン画面を表示してユーザー名をクリックしてユーザー画面に移動()
```
これで合計9個のテストが完成しました。最後に全テストを回してパチパチとやろうとしたら、1個失敗しました。場所はログイン用のユーザー名をSendKeyするところ。しかもこれは再現性が低そうです。とりあえず、SendKeyしていたところを人間的に合わせるためにもClick,Clear,SendKeyに直してこれがオールグリーンになりました。

```C#
element.SendKeys(text);
↓
element.Click();
element.Clear();
element.SendKeys(text);
```
これで当初予定していたリグレッションテストが全部実装しました！

#5回目のモブプロ
そして5回目のモブプロは神回になりました。今までのフラストレーションであるコピペプログラムのリファクタリングフェーズです。モブプロと言いつつ、ドライバーはほぼ僕が1人だったのでですが、今回からgitでもう1人とペアプロ的にも進めてみました。スローステップで、一つ一つの細かいアクションをちゃんと声で説明しながら進めていくスタイルでやったのがめちゃめちゃ良かったです（個人的には[流々舞](https://twitter.com/hunterx2bot/status/610473493826785281)だ！と思いました）。

コードがどんどん読みやすく、明確になっていくのが本当に快適で、楽し過ぎて過去最長の5時間以上やってしまいました。テストコードで直接driverを使っているところを無くすという方針が出来たのですが、かなり減ったけれど無くしきれてはいないので、それは次回となりました。また次回はいよいよ4人でプログラムを回す本当の意味でのモブプロも予定しています。

#6回目のモブプロ
6回目のモブプロは参加者が1人増えたため、しっかりとした振り返りから始めました。またVisual Studio Code Shareでのモブプロをしました。コードシェアは全く問題ありませんでしたが、何故かインテリセンスがきかないなどエディタとしての性能がイマイチでした。完全に並列でプログラムを組めるので、2人が全く別の部分を同時に書き換えることが出来るのは中々に新体験でした。

さて、今回はテストコードからdriverを無くすことが出来ました。その部分はラッパークラスに持っていきました。ここでこのクラスの名前ですが、現状でベストな名前を決められなかったため、そういう時に最善なかなり強烈な名前付けを教えて貰いました。このクラスの名前は**SeleniumWrapperWillBeRenamed**です。この名前を残しておくことで、名前付けがまだだったことを忘れることがなくなると言われて、なるほどなと思いました。そしてテストコードは殆ど「やっていること」を示す関数だけになりました。ここまで見やすくなるとさらにもう一段階綺麗にしたくなるねとなって続きは次回です。

```C#
[TestMethod]
public void トップ画面から移動してマンガ画面を表示()
{
    トップページに移動して画面が変わるのを待つ();
    マンガ一覧画面に移動して画面が変わるのを待つ();
    sw.GetTitle().Is("マンガ一覧|マンガ読んだ！！");
}
```
#7回目のモブプロ
7回目のモブプロは1回間隔が空いたこともあって、プロダクトを含む環境をパワーアップしました。.NET 5対応とNullableの有効化をしました。これでテストコードを含めC#9.0も使えるようになりました。

今回は、遷移の役割を持つclassをまとめることになりました。遷移のclass名はズバリ遷移です。この思い切った名前付けは本当に衝撃的でびっくりしましたが、テストコードとしてはこの上なく分かりやすいものが出来たと思います。実装は早速recordを使いました。

```C#
internal record 画面遷移(SeleniumWrapperWillBeRenamed sw)
{
…
```

また画面内容を取得するclassも作りました。これでテストコードからは完全にSeleinumに依存したコードがなくなりやりたいことを宣言的に書かかれたメソッドだけになりました。

```C#
//作った時の実装
public void トップ画面から移動してマンガ画面を表示()
{
    driver.Navigate().GoToUrl(TopPage);
    WaitState("top");
    driver.Title.Is("トップ|マンガ読んだ！！");
    //移動
    ClickElement("#navbar-search-button");
    //シリーズ画面
    WaitState("series");
    driver.Title.Is("シリーズ一覧|マンガ読んだ！！");
}
```
↓

```C#
//今回リファクタリングまで終わった実装
[TestMethod]
public void トップ画面から移動してマンガ画面を表示()
{
    遷移.トップページへ();
    遷移.シリーズ一覧画面へ();
    取得.ページタイトル().Is("シリーズ一覧|マンガ読んだ！！");
}
```
#8回目のモブプロ
8回目のモブプロはここからどう進めるかの話からしました。前回プロダクトのNullableを有効化したことで、全ソースに手を入れたのでテストの不足も感じていました。またそろそろプロダクトコード側も含めた壊れにくいテストに変えていくか、はたまたリファクタリングを続けてページオブジェクトに育てていくか。結果テストを増やす前に、もう少し壊れにくくすることにしました。

最初にテストを作った時はbootstrap用に使っているclassを指定して要素を特定していました。しかしそれだと画面のレイアウトが少し変わっただけでテストが壊れてしまいます。機能が変わらない限り画面が変わっても壊れないような仕組みに変えていきました。SeleniumWrapperでの指定も全体的にFindElementByCssSelectorにしました。

今日はこの名前を何にするかに殆ど時間を使いました。この会はモブプロでやっていますが、あまり明確な答えを言わないように進めてくれています。最終的には僕がどう名前を付けるで、改めて一つずつ名前を付ける大切さを思い知らされます。全体的に見直して壊れにくい名前付けになりました。

```C#
sw.MakeSelectedByClassName("form-control", 2);
↓
sw.MakeSelected("#NavMenuSearchTarget", (int)SearchTarget.Manga);
```
```html
<div id="wait" data-state="0">
↓
<div id="CurrentPageName" data-name="">
```
#まとめ
このモブプロは隔週で2時間～ぐらいやっていて、若干宿題的にいくつか調べたり実装したりすることはありましたが、基本モブプロの時以外は殆ど時間を取らずに進めていました。このリズムが自分の中ではベストでした。2週間あると、時間を取らないと言いつつ、思考や下調べも出来ます。また、リグレッションテストというあれば便利だけどなくても良いものというのが、テーマとして非常に適していました。実際モブプロの日以外はプロダクトの方をバリバリ進めていました。（ただ4回目の後はしっかり時間を使ってAzure DevOpsの設定をしました。この内容は[Seleniumを使ったシステムテストをAzure DevOpsに導入した話](https://binnmti.hatenablog.com/entry/2020/12/01/000052)に書きました）そしてモブでやるため、知識の獲得や新しい気づきも多く、何より楽しいです。リグレッションテストが終わっても、また別のテスト整備や環境設定まわりなどで続けて行きれば思っています。多謝。 [kazuhito_m](https://twitter.com/kazuhito_m)さん、[Hidari0415](https://twitter.com/Hidari0415)さん、[USHITO Hiroyuki](https://twitter.com/USHITO Hiroyuki)さん。[dairappa](https://twitter.com/dairappa)さん。