What Is a UX Audit and How to Conduct One to Improve Your Website's Usability
July 31, 2024
Unit Testing with Moq
One of our goals at Marathon is to create holistic solutions that address our clients' needs. There are many facets of software development projects that allow us to deliver superior results, one of which is to devote time to creating robust mechanisms for automated testing and validation.
Unit Testing
There are many ways to validate software, one of which is unit testing. This kind of testing focuses on examining isolated chunks, or units, of code. These kinds of tests do not interact directly with infrastructure like databases, files, networks, external services, etc. Unit tests are intended to isolate our code and provide an insulated environment where we can directly test the inputs, outputs, and behaviors contained within the code that the developers can control.
public class Calculator
{
public double Add(double item1, double item2)
{
return item1 + item2;
}
}
An example would be to compose a unit test to evaluate the behavior of a function that performs simple addition. For that kind of unit test, we would provide the numbers that we would like to add and then compare the result that was produced by the unit test with the result that was calculated manually. If the values match, then the test passes. If they do not match, it fails, so we start investigating.
[TestMethod]
public void TestAdditionCalculationPasses()
{
var calculator = new CalculatorExample.Calculator();
var result = calculator.Add(2, 3);
//Assert will compare the result we "know"
//with the one the unit under test returns
Assert.AreEqual(5, result);
}
These unit tests can be executed on demand or as part of an automated software build process. The results of these tests can cause that process to succeed or fail. In either case, it will normally produce a report of each result and provide additional details if there was a failure condition, along with details about what may have caused the test failure. This sort of feedback helps us inform our software development efforts and make improvements so that it becomes more robust.
Mocking with Moq in Unit Testing
When we discuss mocking in relation to unit testing, our intent is to provide stand-in replacements for object classes that are outside the scope of a given unit test.
For example, we may want to test a class method that calculates the total cost for an e-commerce order. Part of that cost calculation includes communicating with one or more external shipping vendor systems. In such a scenario, we are not testing the vendor’s system – only our code that interfaces with it – so we might consider mocking the object that provides the interaction with the vendor system.
To be clear, we do not want to perform a unit test that talks to an external service. If that were to occur it would become an integration test and would no longer be just a unit test.
In this demonstration, we will be using a C# class library, MSTest for unit testing, and the Moq library for mocking. The shipping service will be represented by the IShipper interface. This interface is what we would like to mock:
public class Order
{
public List<OrderItem> OrderItems { get; set; }
public string Address { get; set; }
public double GetItemTotal()
{
return OrderItems.Sum(x => x.Price);
}
public double GetShippingTotal(IShipper shipper)
{
//we need to contact an external service to accomplish this task
var shippingCost =
shipper.CalculateShippingCost(
OrderItems.Sum(x => x.Weight),
this.Address
);
return shippingCost;
}
public double GetOrderTotal(IShipper shipper)
{
var shippingTotal = GetShippingTotal(shipper);
var itemTotal = GetItemTotal();
return shippingTotal + itemTotal;
}
}
For our unit test, we are focusing on the GetOrderTotal method from our Order class object. The shipping calculation is only one part of the behavior. By mocking the shipping calculation portion, we can isolate this unit of code from external influence and test the parts of the code that are under our control.
With a mock in place, we can train the mock on what to return when given one or more specific inputs. Mocking in this way allows us to narrow the scope of our unit tests so that external concerns do not influence the result of our unit test.
[TestMethod]
public void TestOrderTotalCalculation()
{
//now create a mock object that serves in place of our external vendor shipping system
var shippingServiceMock = new Mock<IShipper>();
//then define the substitute behaviors
//here we are telling our mock that any request for CalculateShippingCost can accept
//any parameters that match the expected data types and we will always have it return 45.98
//this can be adjusted as desired and set to reflect specific input values, as well
shippingServiceMock
.Setup(x => x.CalculateShippingCost(
It.IsAny<double>(),
It.IsAny<string>()))
.Returns(45.98);
}
If we were interested in how our system behaves when connected to the vendor’s system, we would consider that an integration test due to the fact that we are testing the entire integration rather than the unit of code that contains the integration.
[TestMethod]
public void TestOrderTotalCalculation()
{
//now create a mock object that serves in place of our external vendor shipping system
var shippingServiceMock = new Mock<IShipper>();
//then define the substitute behaviors
//here we are telling our mock that any request for CalculateShippingCost can accept
//any parameters that match the expected data types and we will always have it return 45.98
//this can be adjusted as desired and set to reflect specific input values, as well
shippingServiceMock
.Setup(x => x.CalculateShippingCost(
It.IsAny<double>(),
It.IsAny<string>()))
.Returns(45.98);
//retrieve a sample order from our manually defined order data
var order = GenerateSampleOrder();
//calculate the subtotal of all items in the order
var orderItemTotal = order.GetItemTotal();
//calculate the order total with shipping included
var orderTotal = order.GetOrderTotal(shippingServiceMock.Object);
//now we ensure that the result from the GetOrderTotal method matches what we manually defined in the mock setup in the beginning of the test
Assert.AreEqual(45.98, orderTotal);
}
An analogy for a unit test might be having a mechanic test whether or not your brakes work by pressing on the brake pedal and observing if the brake calipers respond as expected. An integration test of your brakes would be more akin to taking your vehicle out into the parking lot and slamming on the brakes. There is a significant difference between testing something in isolation versus in more realistic conditions.
Summary
Unit testing along with mocking allows us to isolate our code from external systems and test various scenarios that we may encounter in real-world use. Of course, we cannot foresee all scenarios, but as developers, we can envision many of the most common ones and write unit tests to cover them. This bit of effort helps us catch potential issues before software is deployed and overall helps us think in terms of writing code that are more easily testable.