Mock 框架 Moq 的使用

Mock 框架 Moq 的使用

Intro

Moq 是 .NET 中一個很流行的 Mock 框架,使用 Mock 框架咱們能夠只針對咱們關注的代碼進行測試,對於依賴項使用 Mock 對象配置預期的依賴服務的行爲。html

Moq 是基於 Castle 的動態代理來實現的,基於動態代理技術動態生成知足指定行爲的類型git

在一個項目裏, 咱們常常須要把某一部分程序獨立出來以便咱們能夠對這部分進行測試. 這就要求咱們不要考慮項目其他部分的複雜性, 咱們只想關注須要被測試的那部分. 這裏就須要用到模擬(Mock)技術.github

由於, 請仔細看. 咱們想要隔離測試的這部分代碼對外部有一個或者多個依賴. 因此編寫測試代碼的時候, 咱們須要提供這些依賴. 而針對隔離測試, 並不該該使用生產時用的依賴項, 因此咱們使用模擬版本的依賴項, 這些模擬版依賴項只能用於測試時, 它們會使隔離更加容易.sql

img

綠色的是須要被測試的類,黃色Mock的依賴項c#

——引用自楊旭大佬的博文數組

Prepare

首先咱們須要先準備一下用於測試的類和接口,下面的示例都是基於下面定義的類和方法來作的框架

public interface IUserIdProvider
{
    string GetUserId();
}
public class TestModel
{
    public int Id { get; set; }
}
public interface IRepository
{
    int Version { get; set; }

    int GetCount();

    Task<int> GetCountAsync();

    TestModel GetById(int id);

    List<TestModel> GetList();

    TResult GetResult<TResult>(string sql);

    int GetNum<T>();

    bool Delete(int id);
}

public class TestService
{
    private readonly IRepository _repository;

    public TestService(IRepository repository)
    {
        _repository = repository;
    }

    public int Version
    {
        get => _repository.Version;
        set => _repository.Version = value;
    }

    public List<TestModel> GetList() => _repository.GetList();

    public TResult GetResult<TResult>(string sql) => _repository.GetResult<TResult>(sql);

    public int GetResult(string sql) => _repository.GetResult<int>(sql);

    public int GetNum<T>() => _repository.GetNum<T>();

    public int GetCount() => _repository.GetCount();

    public Task<int> GetCountAsync() => _repository.GetCountAsync();

    public TestModel GetById(int id) => _repository.GetById(id);

    public bool Delete(TestModel model) => _repository.Delete(model.Id);
}

咱們要測試的類型就是相似 TestService 這樣的,而 IRepositoy<TestModel>IUserIdProvider 是屬於外部依賴異步

Mock Method

Get Started

一般咱們使用 Moq 最經常使用的可能就是 Mock 一個方法了,最簡單的一個示例以下:async

[Fact]
public void BasicTest()
{
    var userIdProviderMock = new Mock<IUserIdProvider>();
    userIdProviderMock.Setup(x => x.GetUserId()).Returns("mock");
    Assert.Equal("mock", userIdProviderMock.Object.GetUserId());
}

Match Arguments

一般咱們的方法不少是帶有參數的,在使用 Moq 的時候咱們能夠經過設置參數匹配爲不一樣的參數返回不一樣的結果,來看下面的這個例子:ide

[Fact]
public void MethodParameterMatch()
{
    var repositoryMock = new Mock<IRepository>();
    repositoryMock.Setup(x => x.Delete(It.IsAny<int>()))
        .Returns(true);
    repositoryMock.Setup(x => x.GetById(It.Is<int>(_ => _ > 0)))
        .Returns((int id) => new TestModel()
        {
            Id = id
        });

    var service = new TestService(repositoryMock.Object);
    var deleted = service.Delete(new TestModel());
    Assert.True(deleted);

    var result = service.GetById(1);
    Assert.NotNull(result);
    Assert.Equal(1, result.Id);

    result = service.GetById(-1);
    Assert.Null(result);

    repositoryMock.Setup(x => x.GetById(It.Is<int>(_ => _ <= 0)))
        .Returns(() => new TestModel()
        {
            Id = -1
        });
    result = service.GetById(0);
    Assert.NotNull(result);
    Assert.Equal(-1, result.Id);
}

經過 It.IsAny<T> 來表示匹配這個類型的全部值,經過 It.Is<T>(Expression<Func<bool>>) 來設置一個表達式來斷言這個類型的值

經過上面的例子,咱們能夠看的出來,設置返回值的時候,能夠直接設置一個固定的返回值,也能夠設置一個委託來返回一個值,也能夠根據方法的參數來動態配置返回結果

Async Method

如今不少地方都是在用異步方法,Moq 設置異步方法有三種方式,一塊兒來看一下示例:

[Fact]
public async Task AsyncMethod()
{
    var repositoryMock = new Mock<IRepository>();

    // Task.FromResult
    repositoryMock.Setup(x => x.GetCountAsync())
        .Returns(Task.FromResult(10));
    // ReturnAsync
    repositoryMock.Setup(x => x.GetCountAsync())
        .ReturnsAsync(10);
    // Mock Result, start from 4.16
    repositoryMock.Setup(x => x.GetCountAsync().Result)
        .Returns(10);

    var service = new TestService(repositoryMock.Object);
    var result = await service.GetCountAsync();
    Assert.True(result > 0);
}

還有一個方式也能夠,可是不推薦,編譯器也會給出一個警告,就是下面這樣

repositoryMock.Setup(x => x.GetCountAsync()).Returns(async () => 10);

Generic Type

有些方法會是泛型方法,對於泛型方法,咱們來看下面的示例:

[Fact]
public void GenericType()
{
    var repositoryMock = new Mock<IRepository>();
    var service = new TestService(repositoryMock.Object);

    repositoryMock.Setup(x => x.GetResult<int>(It.IsAny<string>()))
        .Returns(1);
    Assert.Equal(1, service.GetResult(""));

    repositoryMock.Setup(x => x.GetResult<string>(It.IsAny<string>()))
        .Returns("test");
    Assert.Equal("test", service.GetResult<string>(""));
}

[Fact]
public void GenericTypeMatch()
{
    var repositoryMock = new Mock<IRepository>();
    var service = new TestService(repositoryMock.Object);

    repositoryMock.Setup(m => m.GetNum<It.IsAnyType>())
        .Returns(-1);
    repositoryMock.Setup(m => m.GetNum<It.IsSubtype<TestModel>>())
        .Returns(0);
    repositoryMock.Setup(m => m.GetNum<string>())
        .Returns(1);
    repositoryMock.Setup(m => m.GetNum<int>())
        .Returns(2);

    Assert.Equal(0, service.GetNum<TestModel>());
    Assert.Equal(1, service.GetNum<string>());
    Assert.Equal(2, service.GetNum<int>());
    Assert.Equal(-1, service.GetNum<byte>());
}

若是要 Mock 指定類型的數據,能夠直接指定泛型類型,如上面的第一個測試用例,若是要不一樣類型設置不一樣的結果一種是直接設置類型,若是要指定某個類型或者某個類型的子類,能夠用 It.IsSubtype<T>,若是要指定值類型能夠用 It.IsValueType,若是要匹配全部類型則能夠用 It.IsAnyType

Callback

咱們在設置 Mock 行爲的時候能夠設置 callback 來模擬方法執行時的邏輯,來看一下下面的示例:

[Fact]
public void Callback()
{
    var deletedIds = new List<int>();
    var repositoryMock = new Mock<IRepository>();
    var service = new TestService(repositoryMock.Object);
    repositoryMock.Setup(x => x.Delete(It.IsAny<int>()))
        .Callback((int id) =>
        {
            deletedIds.Add(id);
        })
        .Returns(true);

    for (var i = 0; i < 10; i++)
    {
        service.Delete(new TestModel() { Id = i });
    }
    Assert.Equal(10, deletedIds.Count);
    for (var i = 0; i < 10; i++)
    {
        Assert.Equal(i, deletedIds[i]);
    }
}

Verification

有時候咱們會驗證某個方法是否執行,並不須要關注是否方法的返回值,這時咱們能夠使用 Verification 驗證某個方法是否被調用,示例以下:

[Fact]
public void Verification()
{
    var repositoryMock = new Mock<IRepository>();
    var service = new TestService(repositoryMock.Object);

    service.Delete(new TestModel()
    {
        Id = 1
    });

    repositoryMock.Verify(x => x.Delete(1));
    repositoryMock.Verify(x => x.Version, Times.Never());
    Assert.Throws<MockException>(() => repositoryMock.Verify(x => x.Delete(2)));
}

若是方法沒有被調用,就會引起一個 MockException 異常:

verification failed

Verification 也能夠指定方法觸發的次數,好比:repositoryMock.Verify(x => x.Version, Times.Never);,默認是 Times.AtLeastOnce,能夠指定具體次數 Times.Exactly(1) 或者指定一個範圍 Times.Between(1,2, Range.Inclusive),Moq 也提供了一些比較方便的方法,好比Times.Never()/Times.Once()/Times.AtLeaseOnce()/Times.AtMostOnce()/Times.AtLease(2)/Times.AtMost(2)

Mock Property

Moq 也能夠 mock 屬性,property 的本質是方法加一個字段,因此也能夠用 Mock 方法的方式來 Mock 屬性,只是使用 Mock 方法的方式進行 Mock 屬性的話,後續修改屬性值就不會引發屬性值的變化了,若是修改屬性,則要使用 SetupProperty 的方式來 Mock 屬性,具體能夠參考下面的這個示例:

[Fact]
public void Property()
{
    var repositoryMock = new Mock<IRepository>();
    var service = new TestService(repositoryMock.Object);
    repositoryMock.Setup(x => x.Version).Returns(1);
    Assert.Equal(1, service.Version);

    service.Version = 2;
    Assert.Equal(1, service.Version);
}

[Fact]
public void PropertyTracking()
{
    var repositoryMock = new Mock<IRepository>();
    var service = new TestService(repositoryMock.Object);
    repositoryMock.SetupProperty(x => x.Version, 1);
    Assert.Equal(1, service.Version);

    service.Version = 2;
    Assert.Equal(2, service.Version);
}

Sequence

咱們能夠經過 Sequence 來指定一個方法執行屢次返回不一樣結果的效果,看一下示例就明白了:

[Fact]
public void Sequence()
{
    var repositoryMock = new Mock<IRepository>();
    var service = new TestService(repositoryMock.Object);

    repositoryMock.SetupSequence(x => x.GetCount())
        .Returns(1)
        .Returns(2)
        .Returns(3)
        .Throws(new InvalidOperationException());

    Assert.Equal(1, service.GetCount());
    Assert.Equal(2, service.GetCount());
    Assert.Equal(3, service.GetCount());
    Assert.Throws<InvalidOperationException>(() => service.GetCount());
}

第一次調用返回值是1,第二次是2,第三次是3,第四次是拋了一個 InvalidOperationException

LINQ to Mocks

咱們能夠經過 Mock.Of 來實現相似 LINQ 的方式,建立一個 mock 對象實例,指定類型的實例,若是對象比較深,要 mock 的對象比較多使用這種方式可能會必定程度上簡化本身的代碼,來看使用示例:

[Fact]
public void MockLinq()
{
    var services = Mock.Of<IServiceProvider>(sp =>
        sp.GetService(typeof(IRepository)) == Mock.Of<IRepository>(r => r.Version == 1) &&
        sp.GetService(typeof(IUserIdProvider)) == Mock.Of<IUserIdProvider>(a => a.GetUserId() == "test"));

    Assert.Equal(1, services.ResolveService<IRepository>().Version);
    Assert.Equal("test", services.ResolveService<IUserIdProvider>().GetUserId());
}

Mock Behavior

默認的 Mock Behavior 是 Loose,默認沒有設置預期行爲的時候不會拋異常,會返回方法返回值類型的默認值或者空數組或者空枚舉,

在聲明 Mock 對象的時候能夠指定 Behavior 爲 Strict,這樣就是一個"真正"的 mock 對象,沒有設置預期行爲的時候就會拋出異常,示例以下:

[Fact]
public void MockBehaviorTest()
{
    // Make mock behave like a "true Mock",
    // raising exceptions for anything that doesn't have a corresponding expectation: in Moq slang a "Strict" mock;
    // default behavior is "Loose" mock,
    // which never throws and returns default values or empty arrays, enumerable, etc

    var repositoryMock = new Mock<IRepository>();
    var service = new TestService(repositoryMock.Object);
    Assert.Equal(0, service.GetCount());
    Assert.Null(service.GetList());
    
    var arrayResult = repositoryMock.Object.GetArray();
    Assert.NotNull(arrayResult);
    Assert.Empty(arrayResult);

    repositoryMock = new Mock<IRepository>(MockBehavior.Strict);
    Assert.Throws<MockException>(() => new TestService(repositoryMock.Object).GetCount());
}

使用 Strict 模式不設置預期行爲的時候就會報異常,異常信息相似下面這樣:

strict exception

More

Moq 還有一些別的用法,還支持事件的操做,還有 Protected 成員的 Mock,還有一些高級的用法,自定義 Default 行爲等,感受咱們平時可能並不太經常使用,因此上面並無加以介紹,有須要用的能夠參考 Moq 的文檔

上述測試代碼能夠在 Github 獲取 https://github.com/WeihanLi/SamplesInPractice/blob/master/XunitSample/MoqTest.cs

References

相關文章
相關標籤/搜索