wtf512
wtf512

Reputation: 4738

.NET Core how to unit test service?

I have build a WebAPI and want to create a unit test project to have my services tested automatically.

The flow of my WebAPI is simple:

Controller (DI Service) -> Service (DI Repository) -> _repo CRUD

Suppose I have a service like:

public int Cancel(string id) //change status filed to 'n'
{
    var item = _repo.Find(id);
    item.status = "n";
    _repo.Update(item);
    return _repo.SaveChanges();
}

And I want to build a unit test, which just use InMemoryDatabase.

public void Cancel_StatusShouldBeN() //Testing Cancel() method of a service
{
    _service.Insert(item); 

    int rs = _service.Cancel(item.Id);
    Assert.Equal(1, rs);

    item = _service.GetByid(item.Id);
    Assert.Equal("n", item.status);
}

I've searched other related question, found that

You can't use dependency injections on test classes.

I just want to know if there is any other solution to achive my unit test idea?

Upvotes: 14

Views: 20550

Answers (1)

poke
poke

Reputation: 387707

When unit testing, you should just supply all the dependencies of the class you are testing explicitly. That is dependency injection; not having the service construct its dependencies on its own but making it rely on the outer component to provide them. When you are outside of a dependency injection container and inside a unit test where you are manually creating the class you are testing, it’s your responsibility to provide the dependencies.

In practice, this means that you either provide mocks or actual objects to the constructor. For example, you might want to provide a real logger but without a target, a real database context with a connected in-memory database, or some mocked service.

Let’s assume for this example, that the service you are testing looks like this:

public class ExampleService
{
    public ExampleService(ILogger<ExampleService> logger,
        MyDbContext databaseContext,
        UtilityService utilityService)
    {
        // …
    }
    // …
}

So in order to test ExampleService, we need to provide those three objects. In this case, we will do the following for each:

  • ILogger<ExampleService> – we will use a real logger, without any attached target. So any call on the logger will work properly without us having to provide some mock, but we do not need to test the log output, so we do not need a real target
  • MyDbContext – Here, we’ll use the real database context with an attached in-memory database
  • UtilityService – For this, we will create a mock which just setups the utility method we need inside the methods we want to test.

So a unit test could look like this:

[Fact]
public async Task TestExampleMethod()
{
    var logger = new LoggerFactory().CreateLogger<ExampleService>();
    var dbOptionsBuilder = new DbContextOptionsBuilder().UseInMemoryDatabase();

    // using Moq as the mocking library
    var utilityServiceMock = new Mock<UtilityService>();
    utilityServiceMock.Setup(u => u.GetRandomNumber()).Returns(4);

    // arrange
    using (var db = new MyDbContext(dbOptionsBuilder.Options))
    {
        // fix up some data
        db.Set<Customer>().Add(new Customer()
        {
            Id = 2,
            Name = "Foo bar"
        });
        await db.SaveChangesAsync();
    }

    using (var db = new MyDbContext(dbOptionsBuilder.Options))
    {
        // create the service
        var service = new ExampleService(logger, db, utilityServiceMock.Object);

        // act
        var result = service.DoSomethingWithCustomer(2);

        // assert
        Assert.NotNull(result);
        Assert.Equal(2, result.CustomerId);
        Assert.Equal("Foo bar", result.CustomerName);
        Assert.Equal(4, result.SomeRandomNumber);
    }
}

In your specific Cancel case, you want to avoid using any methods of the service you are not currently testing. So if you want to test Cancel, the only method you should call from your service is Cancel. A test could look like this (just guessing the dependencies here):

[Fact]
public async Task Cancel_StatusShouldBeN()
{
    var logger = new LoggerFactory().CreateLogger<ExampleService>();
    var dbOptionsBuilder = new DbContextOptionsBuilder().UseInMemoryDatabase();

    // arrange
    using (var db = new MyDbContext(dbOptionsBuilder.Options))
    {
        // fix up some data
        db.Set<SomeItem>().Add(new SomeItem()
        {
            Id = 5,
            Status = "Not N"
        });
        await db.SaveChangesAsync();
    }

    using (var db = new MyDbContext(dbOptionsBuilder.Options))
    {
        // create the service
        var service = new YourService(logger, db);

        // act
        var result = service.Cancel(5);

        // assert
        Assert.Equal(1, result);
    }

    using (var db = new MyDbContext(dbOptionsBuilder.Options))
    {
        var item = db.Set<SomeItem>().Find(5);
        Assert.Equal(5, item.Id);
        Assert.Equal("n", item.Status);
    }
}

Btw. note that I’m opening up a new database context all the time in order to avoid getting results from the cached entities. By opening a new context, I can verify that the changes actually made it into the database completely.

Upvotes: 25

Related Questions