単体テストを書くクラスでXamarin.*を呼び出すとテストに失敗することがある(Xamarin)

## はじめに

本記事は [Xamarin Advent Calendar 2020](https://qiita.com/advent-calendar/2020/xamarin) の1日目の記事です。
XamarinでxUnit + Moqを使って単体テストを実装する際のコツを紹介します。

## 2020/12/04 追記

コメントでご指摘を頂き、タイトルや本文を変更しました。
Xamarinの経験が浅くてまだ理解できていないため、詳細はコメント欄をご参照ください。

## 環境

- Visual Studio Community 2019 for Mac：8.8 (build 2913)
- xUnit：2.4.0
- Moq：4.14.7
- Xamarin.Essentials：1.5.3.2

## 実装

かんたんなC#のクラスと単体テストを実装します。

### テスト対象クラスの実装

`Xamarin.Essentials.AppInfo.VersionString` を返すだけの `GetAppVersion()` メソッドを持った `FooService` クラスを実装します。

```cs:FooService.cs
using Xamarin.Essentials;

namespace Foo.Services
{
    public interface IFooService
    {
        string GetAppVersion();
    }

    public class FooService : IFooService
    {
        public FooService()
        {
        }

        public string GetAppVersion()
        {
            return AppInfo.VersionString;
        }
    }
}
```

### テストクラスの実装

`GetAppVersion()` メソッドの戻り値が空でないことを確認するだけの単体テストを実装します。

```cs:FooServiceTests.cs
using Foo.Services;
using Xunit;

namespace Foo.UnitTests.Services
{
    public class FooServiceTests
    {
        [Fact]
        public void OutputAppVersion()
        {
            var fooService = CreateDefaultFooService();

            var appVersion = fooService.GetAppVersion();

            Assert.NotEmpty(appVersion);
        }

        private FooService CreateDefaultFooService()
        {
            return new FooService();
        }
    }
}
```

## 単体テストの実行

上記の単体テストを実行すると、以下のエラーで失敗します。

```
Xamarin.Essentials.NotImplementedInReferenceAssemblyException : This functionality is not implemented in the portable version of this assembly. You should reference the NuGet package from your main application project in order to reference the platform-specific implementation.
```

どうやら `Xamarin.*` のクラスを単体テストで呼び出せないことがあるようです。
`Xamarin.*` だと主語が大きいのですが、適切な表現が思いつかないためこのままとさせてください。

## 単体テスト失敗の解決策

このままでは単体テストが実行できないため、解決策を考えます。

### 解決策1: 単体テストから呼び出せないクラスをモックに差し替える

単体テストから呼び出せないクラスはDIして、テスト時にモックへ差し替えるようにします。

`AppInfoService` クラスを新規作成し、 `FooService` クラスから `Xamarin.Essentials` への依存を切り出します。

```cs:AppInfoService.cs
using Xamarin.Essentials;

namespace Foo.Services
{
    public interface IAppInfoService
    {
        string GetAppVersion();
    }

    public class AppInfoService : IAppInfoService
    {
        public AppInfoService()
        {
        }

        public string GetAppVersion()
        {
            return AppInfo.VersionString;
        }
    }
}
```

`FooService` クラスにコンストラクタ経由で `AppInfoService` クラスをDIすれば、 `Xamarin.Essentials` に依存しなくなります。

```diff_c_sharp:FooService.cs
- using Xamarin.Essentials;

namespace Foo.Services
{
    public interface IFooService
    {
        string GetAppVersion();
    }

    public class FooService : IFooService
    {
+         private readonly IAppInfoService appInfoService;

-         public FooService()
+         public FooService(IAppInfoService appInfoService)
        {
+             this.appInfoService = appInfoService;
        }

        public string GetAppVersion()
        {
-             return AppInfo.VersionString;
+             return appInfoService.GetAppVersion();
        }
    }
}
```

単体テストではMoq（モックライブラリ）を使って `IAppInfoService` のモックを生成し、 `FooService` クラスにDIします。

モックで `GetAppVersion()` メソッドの戻り値を `1.0.0` に固定し、アサートで `1.0.0` と一致するか確認します。
Moqはメソッドが何回呼ばれたかかんたんに確認できるので、1回のみ呼ばれたか（ `Times.Once()` ）を確認します。

```diff_c_sharp:FooServiceTests.cs
using Foo.Services;
+ using Moq;
using Xunit;

namespace Foo.UnitTests.Services
{
    public class FooServiceTests
    {
        [Fact]
        public void OutputAppVersion()
        {
+             var mockIAppInfoService = CreateDefaultMockIAppInfoService();
-             var fooService = CreateDefaultFooService();
+             var fooService = CreateDefaultFooService(mockIAppInfoService);

            var appVersion = fooService.GetAppVersion();

-             Assert.NotEmpty(appVersion);
+             Assert.Equal("1.0.0", appVersion);
+             Mock.Get(mockIAppInfoService).Verify(s => s.GetAppVersion(), Times.Once());
        }

-         private FooService CreateDefaultFooService()
+         private FooService CreateDefaultFooService(IAppInfoService appInfoService)
        {
-             return new FooService();
+             return new FooService(appInfoService);
        }

+         private IAppInfoService CreateDefaultMockIAppInfoService()
+         {
+             var mock = Mock.Of<IAppInfoService>(s =>
+             s.GetAppVersion() == "1.0.0"
+             );
+ 
+             return mock;
+         }
    }
}
```

これで単体テストを実行すると、無事に成功します :confetti_ball: 

### 解決策2: Xamarin.Essentials.Interfacesを使う

[コメント](https://qiita.com/uhooi/items/7f7945cdc44aa901114e#comment-4daa7bda707a0f1d945f) で教えていただいたのですが、Xamarin.Essentials.Interfacesというパッケージが非公式で公開されています。
https://www.nuget.org/packages/Xamarin.Essentials.Interfaces/
https://github.com/rdavisau/essential-interfaces

まだ試していませんが、こちらを使うのもよさそうです。

## おわりに

Xamarinでは `Xamarin.*` に依存しないようにビジネスロジックのクラスを実装すると、テスタブルになります。
依存していてもテストする方法はあるようですが、それでも `Xamarin.*` を切り出しておくのは設計パターンとして悪くないと思います。

以上、 [Xamarin Advent Calendar 2020](https://qiita.com/advent-calendar/2020/xamarin) の1日目の記事でした。
明日は [@ytabuchi](https://qiita.com/ytabuchi) さんの記事です。