はじめに
xUnit + Moq でユニットテストを実装する備忘録です。
環境
- Visual Studio Community 2022
- .NET 8.0
- xunit.v3 3.2.2
- xunit.runner.visualstudio 3.1.5
- coverlet.collector 10.0.0
- Moq 4.20.72
ユニットテストの対象クラス
ユーザー ID による検索処理を行える、トランザクションスクリプトなサービスクラスです。他の CRUD 処理はノイズになりそうだったので、省略しています。
UserUseCase.cs
namespace TransactionScriptExampleNet8.Core;
/// <summary>
/// ユーザー関連のビジネスロジックを提供するインターフェースです。
/// </summary>
public interface IUserUseCase
{
/// <summary>
/// 指定されたユーザー ID に対応するユーザー情報を取得します。
/// </summary>
/// <param name="userId">ユーザー ID。</param>
/// <returns><see cref="UserDto"/> オブジェクト。</returns>
UserDto? GetUserById(string userId);
}
/// <summary>
/// ユーザー関連のビジネスロジックを提供します。
/// </summary>
public class UserUseCase : IUserUseCase
{
private readonly IDbSessionFactory _dbSessionFactory;
/// <summary>
/// <see cref="UserUseCase"/> クラスの新しいインスタンスを初期化します。
/// </summary>
public UserUseCase(IDbSessionFactory dbSessionFactory)
{
_dbSessionFactory = dbSessionFactory;
}
/// <inheritdoc/>
public UserDto? GetUserById(string userId)
{
// DB 接続は Dapper などを想定しています。
using (IDbSession dbSession = _dbSessionFactory.CreateDbSession())
{
var sql = "SELECT * FROM Users WHERE user_id = @UserId";
return dbSession
.Query<UserDto>(sql, new { UserId = userId })
.FirstOrDefault();
}
}
}
ユニットテストの実装例
例として正常系のテストパターンを実装しています。テストプロジェクトでは、サマリーはつけず、メソッド名でテスト内容を表現することがお作法らしいです。
UserUseCaseTests.cs
using Moq;
using TransactionScriptExampleNet8.Core;
namespace TransactionScriptExampleNet8.Test;
public class UserUseCaseTests
{
private readonly Mock<IDbSessionFactory> _factoryMock;
private readonly Mock<IDbSession> _dbSessionMock;
private readonly UserUseCase _useCase;
public UserUseCaseTests()
{
_factoryMock = new Mock<IDbSessionFactory>();
_dbSessionMock = new Mock<IDbSession>();
// CreateDbSession() が呼ばれたときに Moq で生成した
// IDbSession の偽物のインスタンスを返すように設定する。
_factoryMock
.Setup(x => x.CreateDbSession())
.Returns(_dbSessionMock.Object);
// UserUseCase に Moq 化した IDbSessionFactory を注入する。
_useCase = new UserUseCase(_factoryMock.Object);
}
[Fact]
public void GetUserById_UserExists_ReturnsUser()
{
// --------------------------------------------------------------------
// Arrange : 準備
// --------------------------------------------------------------------
// 戻り値として返す UserDto を作成する。
var user = new UserDto
{
UserId = "001",
Password = "P@ssw0rd",
UserName = "Taro",
UserGroup = "Admin",
Remarks = "test"
};
// IDbSession.Query<T>() が呼ばれたときに
// UserDto を返すように設定する。
_dbSessionMock
.Setup(x => x.Query<UserDto>(
It.IsAny<string>(),
It.IsAny<object>()))
.Returns([user]);
// --------------------------------------------------------------------
// Act : 実行
// --------------------------------------------------------------------
// GetUserById() を呼び出す。
UserDto? result = _useCase.GetUserById("001");
// --------------------------------------------------------------------
// Assert : 結果の検証
// --------------------------------------------------------------------
// GetUserById() の戻り値を検証する。
Assert.NotNull(result);
Assert.Equal("001", result!.UserId);
// --------------------------------------------------------------------
// Verify : 内部処理の検証
// --------------------------------------------------------------------
// GetUserById() の内部で IDbSessionFactory.CreateDbSession() が
// 1 回だけ呼び出されたことを検証する。
_factoryMock
.Verify(
x => x.CreateDbSession(),
Times.Once);
// GetUserById() の内部で IDbSession.Query<T>() が
// 1 回だけ呼び出されたことを検証する。
_dbSessionMock
.Verify(
x => x.Query<UserDto>(
It.IsAny<string>(),
It.IsAny<object>()),
Times.Once);
// GetUserById() の内部で IDisposable.Dispose() が
// 1 回だけ呼び出されたことを検証する。
_dbSessionMock
.Verify(
x => x.Dispose(),
Times.Once);
}
}
参考
おわりに
正直なところ、ただの CRUD 処理には過剰設計になると思います。複雑な計算がある時など、ちょうどよい塩梅の粒度で実装したいです。