Telerik blogs
How ToT2 Dark_1200x303

JustMock makes it easy to create tests that isolate our code changes, even in “untestable” legacy applications.

Articles that demonstrate automated testing generally assume that you’ll be working with applications that reflect current coding practices, including practices often adopted specifically to make code “testable.” Since we all spend most of our time extending, enhancing and (occasionally) fixing “legacy” applications, that’s not the code we’re typically working with.

In fact, the code we work with is often considered “untestable”—it calls static members or extension methods, has methods that accept classes rather than interfaces, depends on fields declared at the class level, interacts with Framework objects, or works with data pulled from a database using Entity Framework. These practices make it difficult to create tests that only fail because of the changes we make, rather than some because of some change in the code’s environment.

Fortunately, with JustMock it is, in fact, easy to a create an automated unit that does just that, even with legacy applications.

The Method From Hell

As an example, here’s a method from legacy hell that we need to enhance and does all of those “untestable” things:

CustomerContext CustEntities = new CustomerContext();

public decimal CalcShipping(Product prod, int qty, ShippingUrgency urg, DateTime ShipDate)
{
   //…some code
 Customer cust = (from c in CustEntities.Customers
      where c.CustomerId == prod.CustId
      select c).FirstOrDefault();

//…more code

decimal urgencyCost = prod.UrgencyCost(urg);
decimal prodCost = prod.ProdCust();

//…yet more code

   decimal ShippingCost = (urgencyCost 
        + ShippingStrategy.DestCost(cust.Addresses.First(a => a.AddressType == “S”))
        + ShippingStrategy.TimeCost(ShipDate, DateTime.Now)
        + prodCost) * qty;

 //…still more code

  return ShippingCost;
}

Some of the ways that a test could go wrong without it being the fault of our changes are easy to see. The LINQ statement, for example, is using an Entity Framework DbContext object called CustEntities (declared as a field at the top of the class) that pulls data from a database. You might get a test working and then suddenly have it fail, not because of an error in your code but because someone made a change to the database.

But there’s more. One of the methods called from ShippingStrategy is passed DateTime.Now. That creates a classic example of how tests can go wrong: If the test is run on Monday and works, there’s no guarantee that the test will work on Tuesday because that DateTime.Now parameter will be returning a new value.

The methods called the ShippingStrategy class feature a different problem: We don’t know how the code in those methods work—that code might also be date-dependent or extract data from some database we don’t know about. Our test will be more reliable if we can take control of those methods. However, both of those methods are static, which makes it more difficult to override them (in fact, the whole ShippingStrategy class is marked as static).

Even your control over what gets passed to the method from hell is limited by the nature of the Product object the method from hell accepts. Product is a sealed class and doesn’t have an interface—creating a mock version of that class whose properties you can control and methods you can override is out of the question.

Oh, and by the way: That UrgencyCost method called from the Product object—that’s an extension method.

It may seem that building a reliable/repeatable test for this method is impossible. But, with JustMock, you can manage all of these cases.

Some housekeeping is required before you can begin. You must add a test project to your solution using the C# JustMock Test Project template that’s installed with JustMock. (You’ll probably also rename both the default class and the default method inside that class.) Then, before you start writing your tests, you must enable the JustMock profiler from Visual Studio’s Extensions | JustMock menu.

Now you’re ready to build an automated test around the method from hell.

Mocking the Product Class Methods (Built-in or Extension)

The first step in writing a test for this method is to create a mock Product object to pass to it. Normally, if you need a mock object to stand in for a real object, you’ll create a new object that implements the real object’s interface. If the object doesn’t have an interface, you can always inherit from the object’s class and create a new object with the behavior you want. Of course, in either of those cases, you’ll either have to modify the code you’re testing to have it accept an interface instead of a class, or modify the class you’re inheriting from by marking the methods in the class that you want to override as virtual.

With JustMock, you don’t have to do any of those things.

The first step with JustMock in taking control of the Product class is to create an instance of it. Then you pass a lambda expression to the Arrange method of JustMock’s Mock class, referencing the method on your newly created Product object you want to override. Finally, you tack on JustMock’s Returns method to specify the value you want returned from the overridden version of the method.

This example mocks the Product object’s ProdCost method to have the method return 1 whenever it’s called:

Product prod = new Product();
Mock.Arrange(() => prod.ProdCost()).Returns(1);

That still leaves that UrgencyCost extension method called from the Product object. Fortunately, JustMock doesn’t really care that UrgencyCost is an extension method—the mocking code is the same as with a built-in method. So, this example goes one step further and specifies that UrgencyCost is to return 2 when it’s called, but only if the method is passed ShippingUrgency.High:

Mock.Arrange(() => prod.UrgencyCost(ShippingUrgency.High)).Returns(2);

The Product object is now configured for your test and is ready to be passed to the method from hell. However, we still need to take control of the code inside the method from hell.

Mocking Static and Framework Methods

Mocking the static methods on ShippingStrategy is only slightly more difficult. Before you can use the Mock class’s Arrange with a static class, you have to set up the static class using Mock’s SetupStatic method. To do that, you pass SetupStatic the Type object for the static class, along with an optional parameter indicating how much of the class you want to mock (the following example uses StaticConstructor.Mocked to make all the methods “mockable” because, well, why not?).

After that it’s just matter of, once again, passing a lambda expression to the Arrange method referencing which method/parameters combination you want to mock. This example goes a little further and, rather than specifying a particular parameter value, uses one of JustMock’s Matchers. Matchers give you additional flexibility in specifying the parameters passed to a mocked method so you don’t have to hard-code specific values.

In this case, the Arg matcher is used to have calls to the DestCost method return 3 when any Address object is passed to the method:

Mock.SetupStatic(typeof(ShippingStrategy), StaticConstructor.Mocked);
Mock.Arrange(() => ShippingStrategy.DestCost(Arg.IsAny<Address>())).Returns(3);

If you’ve read this far, you can probably guess how to mock built-in Framework objects, like DateTime.Now. The following code has the Now property on the DateTime class always return May 31, 1953. Even though Now is a property (rather than a method as we’ve used so far), it really doesn’t make any difference to the JustMock code:

Mock.Arrange(() => DateTime.Now).Returns(new DateTime.Parse("1953/05/31"));

Mocking Entity Framework

Finally, it’s time to deal with the LINQ call used with the CustomerContext DbContext object created inside the method from hell. Some additional work is required because you need to provide a collection of test data to replace the data that Entity Framework would normally extract from the database.

A method to create and return a collection called dummyCustomers, containing the single Customer to be used in the test (along with an associated Address object), looks like this:

private IList<Customer> GetDummyCustomers()
{
   Address dummyAddress = new Address { CustomerId = 10, AddressType = "S", 
        City = "Regina", Country = "Canada" };

   List<Address> dummyAddresses = new List<Address> { dummyAddress };

   Customer dummyCustomer = new Customer { CustomerId = 1, 
        FullName = "Jason van de Velde", 
        Addresses = dummyAddresses };

   List<Customer> dummyCustomers = new List<Customer> { dummyCustomer };
}

You don’t need to wrap this code in a method but collections like this are often useful in multiple tests. Wrapping the code in a method makes the collection easier to reuse (and, besides, the arrange portion of this test is already cluttered enough with all the other mocking code the method from hell requires).

Now it’s just a matter of instantiating the DbContext object that ShippingCostCalculator uses and leveraging the Arrange method. In this case we want any call to the Customers property on CustomerContext—even on the CustomerContext object used inside the method from hell—return this collection of dummy Customer objects.

That requires using two new methods with Arrange: IgnoreInstance and ReturnsCollection. The ReturnsCollection method causes the GetDummyCustomer method to be used to provide that data; IgnoreInstace ensures that this mock is applied to the CustomerContext object used in the method from hell and not just to the CustomerContext in the test code.

The DbContext mocking code in the test ends up looking like this:

CustomerContext mockedEntities = new CustomerContext();
Mock.Arrange(() => mockedEntities.Customers)
        .IgnoreInstance()
        .ReturnsCollection(GetDummyCustomers());

If we make all of these changes, we’ll have isolated our automated test from changes in the environment (and many tests may not require all of these changes). Now we can enhance the method from hell, run our tests and know that if the test fails then it’s because of our changes.

Here’s what the final version of the test looks like:

[TestMethod]
public void CalcShippingTest()
{
   //Arrange
   ShippingCostCalculator scc = new ShippingCostCalculator();
   DateTime ShipDate = DateTime.Parse("2021/01/01");
         
   //Product methods
   Product prod = new Product();
   prod.CustId = 1;
   Mock.Arrange(() => prod.ProdCost()).Returns(1);
   Mock.Arrange(() => prod.UrgencyCost(ShippingUrgency.High)).Returns(2);

   //static mocking
   Mock.SetupStatic(typeof(ShippingStrategy), StaticConstructor.Mocked);
   Mock.Arrange(() => ShippingStrategy.DestCost(Arg.IsAny<Address>())).Returns(3);

   //.NET Famework
   Mock.Arrange(() => DateTime.Now).Returns(DateTime.Parse("1953/05/31"));

   //Entity Framework
   CustomerContext mockedEntities = new CustomerContext();
   Mock.Arrange(() => mockedEntities.Customers)
                .IgnoreInstance()
                .ReturnsCollection(GetDummyCustomers());

   //Act
   decimal res = scc.CalcShipping(prod, 200, ShippingUrgency.High, ShipDate);

   Assert.AreEqual(1200, res);
}

Hopefully, your legacy methods won’t present quite as many challenges as this method does (final code download is here). But with these tools, you can automate testing both for your legacy code and for any bright, shiny new code you get. Assuming you get some bright, shiny new code, of course.


Peter Vogel
About the Author

Peter Vogel

Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter also writes courses and teaches for Learning Tree International.

Related Posts

Comments

Comments are disabled in preview mode.