質問

Given the following simple service class, in the GetCategories() method, should you test the fact that the categoryRepository.Query() method was called, or should you be setting up a test that keeps a list of categories and returns those?

I guess what I am saying is would mocking the categoryRepository and verifying that it's Query method was called once cover this test case?

public class CategoryService : ValidatingServiceBase, ICategoryService
{
    private readonly IRepository<Category> categoryRepository;
    private readonly IRepository<SubCategory> subCategoryRepository;
    private readonly IValidationService validationService;

    public CategoryService(
        IRepository<Category> categoryRepository,
        IRepository<SubCategory> subCategoryRepository,
        IValidationService validationService)
        : base(validationService)
    {
        this.categoryRepository = categoryRepository;
        this.subCategoryRepository = subCategoryRepository;
        this.validationService = validationService;
    }

    public IEnumerable<Category> GetCategories()
    {
        return categoryRepository.Query().ToList();
    }
}

Sample Test

[Fact]
public void GetCategories_Should_CallRepositoryQuery()
{
    var categoryRepo = new Mock<IRepository<Category>>();
    var service = new CategoryService(categoryRepo.Object, null, null);

    service.GetCategories();

    categoryRepo.Verify(x => x.Query(), Times.Once());
}
役に立ちましたか?

解決

It doesn't matter. In both cases (mock + behavior verification vs stub + assertion) you achieve exactly the same result and require exactly the same level of details about inner workings of class. Stick to whichever one you think is more suited in given scenario.


Unit test you posted is an example of behavior verification. You don't assert any values but instead check whether some method was called. This is especially useful when method call has no visible results (think about logging) or doesn't return any value (obviously). It of course has drawbacks, especially when you do such verification for methods that do return value, and don't check it (as is your case - we'll get to it).

The stubbing and asserting approach uses the collaborators to generate value. It doesn't check whether methods were called (at least not directly, yet such test is performed when you setup stub and that setup works), but instead relies on correct flow of stubbed value.

Let's go with simple example. Say you test a method of your class, PizzaFactory.GetPizza which looks like this:

public Pizza GetPizza()
{
    var dough = doughFactory.GetDough();
    var cheese = ingredientsFactory.GetCheese();
    var pizza = oven.Bake(dough, cheese);
    return pizza;
}

With behavior verification you'd check whether doughFactory.GetDough was called, then ingredientsFactory.GetCheese and finally oven.Bake. Had such calls indeed been made, you'd assume pizza was created. You don't check that your factory returns pizza, but assume it happens if all process' steps were completed. You can already see that drawback I mentioned earlier - I can call all the correct methods but return something else, say:

var dough = doughFactory.GetDough();
var cheese = ingredientsFactory.GetCheese();
var pizza = oven.Bake(dough, cheese);
return garbageBin.FindPizza();

Not the pizza you ordered? Notice that all the correct calls to collaborators happened just as we assumed they would.

With stub + assert approach it all looks similar except instead of verification you have stubbing. You use values generated by earlier collaborators to stub later collaborators (if somehow you get wrong dough or cheese, oven will not return pizza we wanted). The final value is what your method returns and this is what we assert:

doughFactoryStub.Setup(df => dg.GetDough).Return("thick");
ingredientsFactoryStub.Setup(if => if.GetCheese()).Return("double");
var expectedPizza = new Pizza { Name = "Margherita" };
ovenStub.Setup(o => o.Bake("thick", "double")).Return(expectedPizza);

var actualPizza = pizzaFactory.GetPizza();

Assert.That(actualPizza, Is.EqualTo(expectedPizza));

If any part of the process fails (say doughFactory returns normal dough) then the final value will be different and test will fail.

And once again, in my opinion in your example it doesn't matter which approach you use. In any normal environment both methods will verify the same thing and require same level of knowledge about your implementation. To be extra safe you might want to prefer to use the stub + assert approach in case somebody plants you a garbage bin1. But if such thing happens, unit tests are your last problem.


1 Note however that it might not be intentional (especially when complex methods are considered).

他のヒント

Yes, that would be the way.

mockCategoryRepository.Setup(r => r.Query()).Returns(categories)
var actualCategories = new CategoryService(mockCategoryRepository, mock..).GetCategories();
CollectionAssert.AreEquivalent(categories, actualCategories.ToList());

It would look something similar with Moq and NUnit.

What you've presented is a white-box test - an approach also possible in unit testing, but recommended only for simple methods.

In the answer presented by Sruti the service is tested in a black-box sense. The knowledge about the inner method is used only to prepare the test, but you don't verify if the method was called one time, 10 times, or wasn't called at all. Personally, I verify method calls only to verify that some external API that must be stubbed is used correctly (example: sending e-mails). Usually it is sufficient not to care about how a method works, as long as it's producing correct results.

With black-box tests the code and the tests are easier to maintain. With white-box tests, most changes of some internal structure during refactoring of a class usually must be followed by changing test code. In black-box approach you have more freedom to rearrange everything, and still be sure that interface's external behaviour hasn't changed.

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top