単体テストと副作用 - 第2話 オブジェクト指向

# 本シリーズ

[単体テストと副作用 - 第1話 はじまり](https://qiita.com/hiki_neet_p/items/9aeca6766f54f435108e)
**単体テストと副作用 - 第2話 オブジェクト指向**
[単体テストと副作用 - 第3話 DIとモック](https://qiita.com/hiki_neet_p/items/826b3797475451d9f8c9)
[単体テストと副作用 - 第4話 副作用の回避](https://qiita.com/hiki_neet_p/items/d933a89165a9cc437816)
[単体テストと副作用 - 第5話 オブジェクト指向への反乱](https://qiita.com/hiki_neet_p/items/dfe02f254bd8d67a5f62)

# 前回のあらすじ

- `Main` 関数にベタ書きでは、テストコードが書けなかった。
- 副作用を意識すべし。
- 次はにオブジェクト指向っぽく書いてみる。

# お題おさらい

- カンマ区切りの文字列を、標準入力から受け取る。
- 受け取った文字列をカンマで分割し、各値のデータ型を調べる。
  - 空白または `null` のデータ型は `null`。
  - `true` または `false` のデータ型は `bool`。
  - 数値のデータ型は `number`。
  - それ以外の値のデータ型は `string`。
- 各値を `値 : データ型` という形式で、標準出力に1秒間隔で表示する。

# オブジェクト指向？

オブジェクト指向といっても、完璧にできる自信無いお(´・ω・｀)
まあ、何となく適当にやってみよう…。

[GitHubリンク](https://github.com/tana-gh/TestableSample2)

まずは、データ型を定義してみる。

```csharp:TestableSample2/TestableSample2.App/TypeKind.cs

namespace TestableSample2.App
{
    public enum TypeKind
    {
        Null,
        Bool,
        Number,
        String
    }
}
```

続いて、値とデータ型の組をクラスにしよう。

```csharp:TestableSample2/TestableSample2.App/TypedValue.cs
using System;
using System.Text.RegularExpressions;

namespace TestableSample2.App
{
    public class TypedValue
    {
        public string   Value    { get; }
        public TypeKind TypeKind { get; }

        public TypedValue(string value)
        {
            Value = value;

            if (Regex.IsMatch(value, @"^(|null)$"))
            {
                TypeKind = TypeKind.Null;
            }
            else if (Regex.IsMatch(value, @"^(true|false)$"))
            {
                TypeKind = TypeKind.Bool;
            }
            else if (Regex.IsMatch(value, @"^-?[0-9]+(\.[0-9]+)?$"))
            {
                TypeKind = TypeKind.Number;
            }
            else
            {
                TypeKind = TypeKind.String;
            }
        }

        public void WriteLine()
        {
            Console.WriteLine($"{Value} : {ToString(TypeKind)}");
        }

        private string ToString(TypeKind typeKind)
        {
            switch (typeKind)
            {
                case TypeKind.Null:
                    return "null";

                case TypeKind.Bool:
                    return "bool";

                case TypeKind.Number:
                    return "number";

                case TypeKind.String:
                    return "string";
                    
                default:
                    return "";
            }
        }
    }
}
```

そうしたら、プログラムの流れを書いて、と。

```csharp:TestableSample2/TestableSample2.App/MainFlow.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace TestableSample2.App
{
    public static class MainFlow
    {
        public static async Task Run()
        {
            var typedValues = ReadTypedValues();

            foreach (var tv in typedValues)
            {
                await Task.Delay(1000);
                tv.WriteLine();
            }
        }

        private static IEnumerable<TypedValue> ReadTypedValues()
        {
            Console.Write("カンマ区切りの値を入力：");
            var valueStrs = Console.ReadLine();

            return valueStrs.Split(",", StringSplitOptions.None)
                            .Select(x => new TypedValue(x));
        }
    }
}
```

```csharp:TestableSample2/TestableSample2.App/Program.cs
using System.Threading.Tasks;

namespace TestableSample2.App
{
    internal class Program
    {
        private static async Task Main(string[] args)
        {
            await MainFlow.Run();
        }
    }
}
```

これで、テストコードが書けるようになったのだろうか…？

```csharp:TestableSample2/TestableSample2.Test/Test_TypedValue.cs
using System.Collections.Generic;
using Xunit;
using TestableSample2.App;

namespace TestableSample2.Test
{
    public class Test_TypedValue
    {
        [Theory]
        [MemberData(nameof(Constructor_Data))]
        public void Constructor(TypeKind expected, string value)
        {
            var typedValue = new TypedValue(value);

            Assert.Equal(value   , typedValue.Value);
            Assert.Equal(expected, typedValue.TypeKind);
        }

        public static IEnumerable<object[]> Constructor_Data()
        {
            return new[] {
                new object[] { TypeKind.Null  , ""        },
                new object[] { TypeKind.Null  , "null"    },
                new object[] { TypeKind.Bool  , "true"    },
                new object[] { TypeKind.Bool  , "false"   },
                new object[] { TypeKind.Number, "0.0"     },
                new object[] { TypeKind.Number, "789"     },
                new object[] { TypeKind.Number, "-12.345" },
                new object[] { TypeKind.String, "a123"    },
                new object[] { TypeKind.String, "$#@?+"   }
            };
        }
    }
}
```

(＾ω＾)

ここでは、テストフレームワークは「xUnit.net」を使っている。
`MemberData` 属性で指定された `Constructor_Data` メソッドは、テストである `Constructor` メソッドに引数を渡す役割がある。
ここで定義された9個の配列の分だけテストが走り、全て通った。

やったお。
テストコードが書けたお(＾ω＾)
女神さま、見ていてくれたかな。

『…きこえますか…今…あなたのゾウリムシ並みの脳味噌に…直接呼びかけています…』

ゾウリムシって単細胞生物では…(´・ω・｀)

『…あなたの書いたコードには…2つの欠陥があります…。責務が分離できていないこと…。副作用を回避できていないこと…。では…順を追って見てゆきましょう…』

# 責務の分離

『…`TypedValue` クラスを見てください…この部分です…』

```csharp:TestableSample2/TestableSample2.App/TypedValue.cs
        //...

        public void WriteLine()
        {
            Console.WriteLine($"{Value} : {ToString(TypeKind)}");
        }

        //...
```

『…`TypedValue` クラスは何のためのクラスですか…？』

えっと…。
`TypedValue` クラスは、文字列値とそのデータ型の情報を持ったクラスだお。
コンストラクタでは文字列値を受け取って、データ型を自動で判別してる。
あとは、文字列表現を生成する機能もあるね。

『…このクラスは…値とデータ型の組の情報と…それにまつわる機能で構成されるべきです…。不要な責務は…排除しなくてはなりません…』

ふむ。
つまり、上述のコンソール出力は、このクラスの責務ではない、と。

『…その通りです…。更に言えば…このコンソール出力は副作用です…。この種の副作用は…テストコードではテストできません…』

そういえば、`TypedValue` の文字列表現に関するテストが書けてないお…(´・ω・｀)

『…テストを妨げる副作用は…何としても回避するのです…』

# 副作用の回避？

『…前述の通り…副作用はテストが難しいか…または全くテストできません…。副作用の塊である `MainFlow` クラスを見てみましょう…』

```csharp:TestableSample2/TestableSample2.App/MainFlow.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace TestableSample2.App
{
    public static class MainFlow
    {
        public static async Task Run()
        {
            var typedValues = ReadTypedValues();

            foreach (var tv in typedValues)
            {
                await Task.Delay(1000);
                tv.WriteLine();
            }
        }

        private static IEnumerable<TypedValue> ReadTypedValues()
        {
            Console.Write("カンマ区切りの値を入力：");
            var valueStrs = Console.ReadLine();

            return valueStrs.Split(",", StringSplitOptions.None)
                            .Select(x => new TypedValue(x));
        }
    }
}
```

この `MainFlow` クラスは、プログラム全体の流れを記述する static なクラスだお。
標準入力から文字列を受け取り、`TypedValue` に加工して、1秒ごとに標準出力する。

『…`TypedValue` への変換以外は副作用ですね…』

そっか。
だから全くテストできなかったんだね。
…？
さっき女神さまが言った、「副作用の回避」ってどうやるんだお(´・ω・｀)？
他のクラスに副作用のある責務をまとめたとして、その機能を使ったら、使った側のクラスもテストできなくなるんでは…？

『…あなたに"3種の神器"を与えます…。即ち…"テストフレームワーク"…"テストダブル"…"依存性の注入"です…。次回までに…使い方を覚えてくるのです…』

宿題が出たお(・ω・)

～ 次回へ続く ～

# ここまでのまとめ

- オブジェクト指向は責務を意識すると、機能を明確に分離しやすい。
- 副作用を回避する方法がある？
- そのためには、テストフレームワーク、テストダブル、依存性の注入を覚える必要がある。

# 本シリーズ

[単体テストと副作用 - 第1話 はじまり](https://qiita.com/hiki_neet_p/items/9aeca6766f54f435108e)
**単体テストと副作用 - 第2話 オブジェクト指向**
[単体テストと副作用 - 第3話 DIとモック](https://qiita.com/hiki_neet_p/items/826b3797475451d9f8c9)
[単体テストと副作用 - 第4話 副作用の回避](https://qiita.com/hiki_neet_p/items/d933a89165a9cc437816)
[単体テストと副作用 - 第5話 オブジェクト指向への反乱](https://qiita.com/hiki_neet_p/items/dfe02f254bd8d67a5f62)