Use of mock framework MOQ

Time:2021-7-30

Use of mock framework MOQ

Intro

MOQ is a very popular mock framework in. Net. Using the mock framework, we can only test the code we are concerned about, and use the mock object to configure the expected behavior of dependent services for dependencies.

MOQ is based onCastleBased on the dynamic agent technology, the types that meet the specified behavior are dynamically generated

In a project, we often need to separate some programs so that we can test this part. This requires us not to consider the complexity of the rest of the project, but to focus on the part that needs to be tested. Here we need to use mock technology

Because, please look carefully. This part of the code we want to isolate the test has one or more external dependencies. Therefore, when writing the test code, we need to provide these dependencies. For isolation testing, we should not use the dependencies used in production, so we use the dependencies of the simulation version, which can only be used for testing, They make isolation easier

img

greenIs the class that needs to be tested,yellowyesMockDependencies for

——Quoted from Yang Xu’s blog

Prepare

First, we need to prepare the classes and interfaces for testing. The following examples are based on the classes and methods defined below

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

    int GetCount();

    Task GetCountAsync();

    TestModel GetById(int id);

    List GetList();

    TResult GetResult(string sql);

    int GetNum();

    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 GetList() => _repository.GetList();

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

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

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

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

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

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

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

The type we want to test is similarTestServiceThat’s right, andIRepositoyandIUserIdProviderIs it an external dependency

Mock Method

Get Started

Generally, the most common method we use MOQ is mock. The simplest example is as follows:

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

Match Arguments

Usually, many of our methods have parameters. When using MOQ, we can set parameter matching to return different results for different parameters. Take the following example:

[Fact]
public void MethodParameterMatch()
{
    var repositoryMock = new Mock();
    repositoryMock.Setup(x => x.Delete(It.IsAny()))
        .Returns(true);
    repositoryMock.Setup(x => x.GetById(It.Is(_ => _ > 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(_ => _ <= 0)))
        .Returns(() => new TestModel()
        {
            Id = -1
        });
    result = service.GetById(0);
    Assert.NotNull(result);
    Assert.Equal(-1, result.Id);
}

adoptIt.IsAnyTo represent all values that match this type, throughIt.Is(Expression>)To set an expression to assert the value of this type

From the above example, we can see that when setting the return value, you can directly set a fixed return value, set a delegate to return a value, or dynamically configure the return result according to the method parameters

Async Method

Asynchronous methods are used in many places. There are three ways for MOQ to set asynchronous methods. Let’s take a look at an example:

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

    // 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);
}

Another method is OK, but it is not recommended. The compiler will also give a warning, which is as follows

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

Generic Type

Some methods will be generic methods. For generic methods, let’s look at the following examples:

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

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

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

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

    repositoryMock.Setup(m => m.GetNum())
        .Returns(-1);
    repositoryMock.Setup(m => m.GetNum>())
        .Returns(0);
    repositoryMock.Setup(m => m.GetNum())
        .Returns(1);
    repositoryMock.Setup(m => m.GetNum())
        .Returns(2);

    Assert.Equal(0, service.GetNum());
    Assert.Equal(1, service.GetNum());
    Assert.Equal(2, service.GetNum());
    Assert.Equal(-1, service.GetNum());
}

If you wantMockFor data of specified types, you can directly specify generic types, such as the first test case above. If you want to set different results for different types, one is to directly set types. If you want to specify a type or a subclass of a type, you can useIt.IsSubtype, if you want to specify a value type, you can useIt.IsValueType, if you want to match all types, you can useIt.IsAnyType

Callback

When setting mock behavior, we can set callback to simulate the logic during method execution. Let’s take a look at the following example:

[Fact]
public void Callback()
{
    var deletedIds = new List();
    var repositoryMock = new Mock();
    var service = new TestService(repositoryMock.Object);
    repositoryMock.Setup(x => x.Delete(It.IsAny()))
        .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

Sometimes we can verify whether a method is executed without paying attention to the return value of the methodVerificationVerify whether a method is called, for example:

[Fact]
public void Verification()
{
    var repositoryMock = new Mock();
    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(() => repositoryMock.Verify(x => x.Delete(2)));
}

If the method is not called, anMockExceptionException:

verification failed

VerificationYou can also specify the number of times the method is triggered, for example:repositoryMock.Verify(x => x.Version, Times.Never);, default isTimes.AtLeastOnce, you can specify a specific number of timesTimes.Exactly(1)Or specify a rangeTimes.Between(1,2, Range.Inclusive), MOQ also provides some convenient methods, such asTimes.Never()/Times.Once()/Times.AtLeaseOnce()/Times.AtMostOnce()/Times.AtLease(2)/Times.AtMost(2)

Mock Property

MOQ can also use the mock attribute. The essence of property is to add a field to the method, so you can also use the mock method to mock the attribute. If you only use the mock method to mock the attribute, subsequent modification of the attribute value will not cause changes in the attribute value. If you modify the attribute, you should use theSetupPropertyFor the mock attribute, refer to the following example:

[Fact]
public void Property()
{
    var repositoryMock = new Mock();
    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();
    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

We can passSequenceTo specify the effect of a method executing multiple times and returning different results. See the example:

[Fact]
public void Sequence()
{
    var repositoryMock = new Mock();
    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(() => service.GetCount());
}

The return value of the first call is 1, the second is 2, the third is 3, and the fourth is throwing oneInvalidOperationException

LINQ to Mocks

We can passMock.OfTo implement a method similar to LINQ, create a mock object instance and specify the type of instance. If the object is deep and there are more objects to mock, using this method may simplify your code to a certain extent. Let’s take a look at the use example:

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

    Assert.Equal(1, services.ResolveService().Version);
    Assert.Equal("test", services.ResolveService().GetUserId());
}

Mock Behavior

The default mock behavior isLooseIf the enumeration method does not return null values, or if the enumeration method does not return null values by default,

In statementMockObject, you can specify behavior asStrict, this is a“Real”An exception will be thrown when the expected behavior of the mock object is not set. An example is as follows:

[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();
    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(MockBehavior.Strict);
    Assert.Throws(() => new TestService(repositoryMock.Object).GetCount());
}

useStrictWhen the mode does not set the expected behavior, an exception will be reported. The exception information is similar to the following:

strict exception

More

There are other uses of MOQ. It also supports the operation of events, the mock of protected members, some advanced uses, custom default behavior, etc. it seems that we may not use it very often, so it is not introduced above. If you need to use it, please refer to the MOQ documentation

The above test code can be obtained at GitHubhttps://github.com/WeihanLi/SamplesInPractice/blob/master/XunitSample/MoqTest.cs

References

Recommended Today

Rust practice – using socket networking API (II)

In the previous section, we have implemented a minimum runnable version. The reason for using rust instead of C is that rust has the necessary abstraction ability and can achieve the same performance as C. In this section, we do the necessary encapsulation for the code in the previous section. By the way, we can […]