最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

c# - How to write a conceptually correct unit test for API method with Linq? - Stack Overflow

programmeradmin2浏览0评论

I'm learning how to write unit tests, please don't judge heavily. I have an API method below, written in C# for .NET 8.0, which I want to test. I use XUnit, Entity Framework Core 8, SQL Server 2019. Currently I'm rewriting it to separate the data layer, so I will not have to moq DB context.

[HttpGet("GetABunch")]
public async Task<ActionResult<TodoItem>> GetABunch()
{
    string[] myAnimals = { "dog", "cat" };

    var animals = await _context.TodoItems
        .Where(i => myAnimals.Contains(i.Name))
        .ToListAsync();

    return Ok(animals);
}

My unit test is not working. I'm getting an error:

System.ArgumentException : Can not instantiate proxy of class: TodoApiDb.Models.TodoContext.
Could not find a parameterless constructor. (Parameter 'constructorArguments')

But before fighting this error, and creating context constructor without parameters, I believe my test is conceptually wrong, please help to fix it.

Here's my unit test:

[Fact]
public async Task GetABunch_ReturnsFilteredTodoItems()
{
    // Arrange
    var mockData = new List<TodoItem>
    {
        new TodoItem { Id = 1, Name = "dog" },
        new TodoItem { Id = 2, Name = "cat" },
        new TodoItem { Id = 3, Name = "bird" }
    }.AsQueryable();

    var mockSet = new Mock<DbSet<TodoItem>>();
    mockSet.As<IQueryable<TodoItem>>().Setup(m => m.Provider).Returns(mockData.Provider);
    mockSet.As<IQueryable<TodoItem>>().Setup(m => m.Expression).Returns(mockData.Expression);
    mockSet.As<IQueryable<TodoItem>>().Setup(m => m.ElementType).Returns(mockData.ElementType);
    mockSet.As<IQueryable<TodoItem>>().Setup(m => m.GetEnumerator()).Returns(mockData.GetEnumerator());
    mockSet.As<IAsyncEnumerable<TodoItem>>().Setup(m => m.GetAsyncEnumerator(It.IsAny<CancellationToken>()))
        .Returns(new TestAsyncEnumerator<TodoItem>(mockData.GetEnumerator()));

    var mockContext = new Mock<TodoContext>();
    //mockContext.Setup(c => c.TodoItems).Returns(mockSet.Object);
    mockContext.Setup(c => c.Set<TodoItem>()).Returns(mockSet.Object);

    var controller = new TodoItemsController(mockContext.Object);

    // Act
    var result = await controller.GetABunch();

    // Assert
    var actionResult = Assert.IsType<OkObjectResult>(result.Result);
    var returnedItems = Assert.IsType<List<TodoItem>>(actionResult.Value);
    Assert.Equal(2, returnedItems.Count);
    Assert.Contains(returnedItems, i => i.Name == "dog");
    Assert.Contains(returnedItems, i => i.Name == "cat");
}

Testing app code on GitHub.

I'm learning how to write unit tests, please don't judge heavily. I have an API method below, written in C# for .NET 8.0, which I want to test. I use XUnit, Entity Framework Core 8, SQL Server 2019. Currently I'm rewriting it to separate the data layer, so I will not have to moq DB context.

[HttpGet("GetABunch")]
public async Task<ActionResult<TodoItem>> GetABunch()
{
    string[] myAnimals = { "dog", "cat" };

    var animals = await _context.TodoItems
        .Where(i => myAnimals.Contains(i.Name))
        .ToListAsync();

    return Ok(animals);
}

My unit test is not working. I'm getting an error:

System.ArgumentException : Can not instantiate proxy of class: TodoApiDb.Models.TodoContext.
Could not find a parameterless constructor. (Parameter 'constructorArguments')

But before fighting this error, and creating context constructor without parameters, I believe my test is conceptually wrong, please help to fix it.

Here's my unit test:

[Fact]
public async Task GetABunch_ReturnsFilteredTodoItems()
{
    // Arrange
    var mockData = new List<TodoItem>
    {
        new TodoItem { Id = 1, Name = "dog" },
        new TodoItem { Id = 2, Name = "cat" },
        new TodoItem { Id = 3, Name = "bird" }
    }.AsQueryable();

    var mockSet = new Mock<DbSet<TodoItem>>();
    mockSet.As<IQueryable<TodoItem>>().Setup(m => m.Provider).Returns(mockData.Provider);
    mockSet.As<IQueryable<TodoItem>>().Setup(m => m.Expression).Returns(mockData.Expression);
    mockSet.As<IQueryable<TodoItem>>().Setup(m => m.ElementType).Returns(mockData.ElementType);
    mockSet.As<IQueryable<TodoItem>>().Setup(m => m.GetEnumerator()).Returns(mockData.GetEnumerator());
    mockSet.As<IAsyncEnumerable<TodoItem>>().Setup(m => m.GetAsyncEnumerator(It.IsAny<CancellationToken>()))
        .Returns(new TestAsyncEnumerator<TodoItem>(mockData.GetEnumerator()));

    var mockContext = new Mock<TodoContext>();
    //mockContext.Setup(c => c.TodoItems).Returns(mockSet.Object);
    mockContext.Setup(c => c.Set<TodoItem>()).Returns(mockSet.Object);

    var controller = new TodoItemsController(mockContext.Object);

    // Act
    var result = await controller.GetABunch();

    // Assert
    var actionResult = Assert.IsType<OkObjectResult>(result.Result);
    var returnedItems = Assert.IsType<List<TodoItem>>(actionResult.Value);
    Assert.Equal(2, returnedItems.Count);
    Assert.Contains(returnedItems, i => i.Name == "dog");
    Assert.Contains(returnedItems, i => i.Name == "cat");
}

Testing app code on GitHub.

Share Improve this question edited Mar 30 at 6:59 EricSchaefer 26.4k22 gold badges74 silver badges105 bronze badges asked Mar 29 at 19:28 sam sergiy kloksam sergiy klok 64413 silver badges27 bronze badges
Add a comment  | 

1 Answer 1

Reset to default 4

Well, this is opinionated and some people will think I am the devil, because I don't like their shiniest toy...

What you really need to do is to drop Moq.

Look at your code. You wrote two lines of code. And then 5+ lines of Moq code. For no good reason. All your test is testing now is that Moq has no bugs, and the c# return statement works. Because the two lines you wrote and that should be tested, don't even get called any more. And whether they actually work on a real database is anyone's guess, because you did not test it.

What you should test, is whether your code does what you want it to do. I know people have told you that databases are these far away dependencies in far away lands on for away servers. They don't have to be. You can just as easily spin up a SQLite in-memory database, that only exists for the duration of your one test. It probably takes less lines than all your moq-ing and it actually tests the code you wrote on a real database, instead of replacing it with Moq.

Mocking is a really powerful tool and there will be instances where the state you need for your test cannot produced in a "legal" way. Then Moq (or other frameworks) will help. You will know it, when you see it. In all other instances, Moq (or other frameworks) are a crutch holding you back from actually testing your code.

If you ever change your database tables, you have to rework all your unit tests. The point of a test is to still work after you have refactored your code, assuming you have refactored it correctly. This Moq stuff is like glueing Legos together. It will break the instance you change your actual logic. It's the anti-thesis to a good test.

Sorry, I am sure someone else will come by, tell you how to fix this error and properly mock/moq a database connection. Maybe this is what you want. Just remember, it is not your job to test Moq, it is your job to test your code.

How can I create an in memory sqlite database?

DBContext has a constructor that takes an existing connection: https://learn.microsoft/en-us/dotnet/api/system.data.entity.dbcontext.-ctor?view=entity-framework-6.2.0

发布评论

评论列表(0)

  1. 暂无评论