はじめに
本記事は Xamarin Advent Calendar 2020 の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
クラスを実装します。
using Xamarin.Essentials;
namespace Foo.Services
{
public interface IFooService
{
string GetAppVersion();
}
public class FooService : IFooService
{
public FooService()
{
}
public string GetAppVersion()
{
return AppInfo.VersionString;
}
}
}
テストクラスの実装
GetAppVersion()
メソッドの戻り値が空でないことを確認するだけの単体テストを実装します。
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
への依存を切り出します。
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
に依存しなくなります。
- 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()
)を確認します。
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;
+ }
}
}
これで単体テストを実行すると、無事に成功します
解決策2: Xamarin.Essentials.Interfacesを使う
コメント で教えていただいたのですが、Xamarin.Essentials.Interfacesというパッケージが非公式で公開されています。
https://www.nuget.org/packages/Xamarin.Essentials.Interfaces/
https://github.com/rdavisau/essential-interfaces
まだ試していませんが、こちらを使うのもよさそうです。
おわりに
Xamarinでは Xamarin.*
に依存しないようにビジネスロジックのクラスを実装すると、テスタブルになります。
依存していてもテストする方法はあるようですが、それでも Xamarin.*
を切り出しておくのは設計パターンとして悪くないと思います。
以上、 Xamarin Advent Calendar 2020 の1日目の記事でした。
明日は @ytabuchi さんの記事です。