Do I Really Have to Unit Test This Method?

You might write a simple void method that takes some arguments, validates those arguments, then makes a call to insert that data into a database.

To unit test the validation step, you might go ahead and mock the database, injecting it into your class with constructor DI of some sort.

With this mock in place, you can spy to make sure that various arguments pass through your validation (i.e. no exceptions are thrown) and the Save() method is ultimately called on the mock.

With Moq, it would look something like this:

mockDbContext.Verify(x => x.SaveChanges())

Is this testing strategy sufficient? Do you even need to test this method if it’s only a few lines of code? Are you only testing for the sake of coverage?

Focus On Behavior

If you’re focusing on making sure exceptions aren’t thrown and that your dbContext.SaveChanges() is called, you might be too focused on testing implementation over behavior.

By behavior, I mean what the code should do rather than how it should do it. The full incarnation of this style of testing is BDD (Behavior Driven Development).

Examples of behavior are

Example

Let’s look at an example of some tests we might write to lock down the behavior of a simple void method.

We’ll imagine that this class is part of a larger slice of functionality.

public class CustomerManagement {
    private readonly ICrmDbContext _db;
    public CustomerManagement(ICrmDbContext db) {
        _db = db;
    }
    public void AddNewCustomer(Customer customer) {
        if (customer.CustomerMarket == String.Empty) {
            customer.CustomerMarket = "Default";
        }
        if (!String.IsNullOrEmpty(customer.CustomerFirstName)
            && customer.CustomerFirstName.Length > 50) {
            customer.CustomerFirstName.Substring(0, 49);
        }
        if (customer.BillToAddressId == null) {
            throw new Exception("Need a BillToAddress");
        }
        if (customer.CustomerLastName == "Sleazy") {
            throw new Exception("We don't sell to this guy");
        }

        _db.Customers.Add(customer);
        _db.SaveChanges();
    }
}

A test that is too focused on the implementation of this class might look like this:

[Fact]
public void Mock_db_save_changes_is_called() {
    var mockCrmDbContext = new Mock<ICrmDbContext>() { 
        DefaultValue = DefaultValue.Mock 
    };

    var newCustomer = new Customer() {
        CustomerId = 1,
        CustomerFirstName = "William",
        CustomerLastName = "Wonka",
        CustomerMarket = "Chocolate",
        BillToAddressId = 10,
        ShipToAddressId = 20
    };

    CustomerManagement crm = new CustomerManagement(mockCrmDbContext.Object);
    crm.AddNewCustomer(newCustomer);

    mockCrmDbContext.Verify(x => x.SaveChanges());
}

A test that focused more on the behavior of this class might look like this:

[Fact]
public void Customer_with_empty_market_gets_default_value() {
    var mockCrmDbContext = new Mock<ICrmDbContext>();

    string actualCustomerMarket = string.Empty;
    mockCrmDbContext.Setup(x => x.Customers.Add(It.IsAny<Customer>()))
        // Spy on CustomerMarket submitted to DB
        .Callback((Customer c) => actualCustomerMarket = c.CustomerMarket);

    var newCustomer = new Customer() {
        // Salesperson didn't enter the CustomerMarket
        CustomerMarket = String.Empty,
    };

    CustomerManagement crm = new CustomerManagement(mockCrmDbContext.Object);
    crm.AddNewCustomer(newCustomer);
    
    // Verify CustomerMarket = "Default"
    actualCustomerMarket.Should().Be("Default");
}

Even though this test was difficult to write and it’s apparent that we need to separate concerns later - it is written to describe the behavior we are testing. It helps us to stay focused on how this component contributes to the wider functionality of the application or feature we’re working on.

Summary

We began by asking whether testing a method that seems pretty simple, with not a lot of code, is worth the effort. Setting up a mock to test the collaboration the SUT has with a dependency seemed like a good idea.

Sometimes we can lose focus on why we’re testing some code in the first place. It helps to focus on the behavior that we’re trying to achieve with the SUT.

To focus more on testing behavior, it might just require a change in the terminology and variable naming in your test code. It could also require larger changes in the way that you expose the internals of the feature you’re working on.

Hope you enjoyed this short post. Stay tuned for more on testing better with .NET. Be sure to sign up for my newsletter and feel free to contact me if you have any feedback or questions!

Code here


Related Posts:


Sources:

Tweet
comments powered by Disqus