How ToT2 Dark_1200x303

What does unit testing look like after you’ve refactored a legacy application to make it easier and cheaper to maintain?

Most developers spend most of their time extending, enhancing, modifying and (occasionally) fixing “legacy” applications: what’s called software maintenance. Applying unit testing in that environment can seem difficult or even impossible. It’s not.

In an earlier post, I looked at a typical “legacy” application and a typical maintenance activity: extending a shipping cost method to handle multiple shipping methods, including the current shipping method. In that post, I walked through how that method could be refactored to create a better design that would both lower maintenance costs and create a reusable “shipping cost calculator” component as a side effect. Because I took advantage of Visual Studio’s tools, that refactoring would have taken about an hour (an hour and a half at the outside).

And, yes, that design would also let me implement unit testing. So that’s what this post is about: What does unit testing look like after you’ve refactored a legacy application to make it easier and cheaper to maintain?

In this case, that means testing the objects that resulted from my refactoring:

  • A shipping calculator, which is called from the original application and controls the process
  • A set of costing objects for each shipping method (e.g., FedExShip, USPSShip, etc.)
  • A “factory” class that selects and configures the right shipping costing object

As I said in that earlier post, you might have gone with a simpler design. Regardless of the level of refactoring you did, you’re now ready to start testing.

First Step: Designing Tests

Because the company wants to keep the original shipping method, before writing any tests I need to test the original application to determine what currently counts as a right answer. The original software specification and any existing test plans will help me pick my test cases, but I’ll probably have to try out the application myself. This could be the last manual regression test this part of the application will ever see.

When I’m finished this stage, I’ll have a set of test results that I can use to prove that my new version still does what the existing version does (and I may also have got a start on generating test cases for the other shipping methods).

I then need to enhance my test data. The results that I got in my first pass included both the cost related to the shipping method (code that’s now in a class called OriginalShip) and all other shipping costs (the code left in the original method, called CalcShipCost). For all my original method test cases, I need to determine what data the code in my OriginalShip object should be returning.

I’ll still need to hang onto my original “total ship cost” numbers because, after getting my costing objects to work, I’ll need to test “the whole process”: No one will care if OriginalShip does its part well if the result from shipping calculator as a whole is wrong.

Generating this second level of test data gives me another opportunity to check if my refactoring was done right. It wouldn’t be surprising, after all, if I discovered that, because I didn’t fully understand what parts were related to the shipping method and what parts were not, my first costing object isn’t quite right. And, again, this will probably give me a start on generating the test data for my other costing objects (USPSShip, FedExShip, etc.).

This also positions my application to support two levels of test. At the first level (the unit-test level), I can test my costing objects to make sure they work right—and set myself up to test any new costing objects down the line. When I know that any costing object works, I can move up to my second level of testing: component-level tests that prove my costing objects work with the shipping calculator.

There’s a real benefit to having a component-level test: The calculator returns the full shipping cost, the number that my users see and what gets stored in the database—the “business answer.” That’s a lot easier to check and to get my users to sign off on than the intermediary result from my costing objects.

Second Step: Writing the Unit-Level Tests

I start automating my regression tests by adding a new project to my solution, using the C# JustMock Test Project template that’s installed into Visual Studio when you install JustMock. That project includes a default test class named JustMockTest that I’ll rename to something more specific.

Some fixup is required at this point: I have to add to my test project a reference to my application. Eventually, I’ll need some mock objects so I also enable the JustMock profiler (Extensions | JustMock | Enable Profiler).

Testing the Factory

With my first tests, I test the factory object that selects and configures my shipping costing objects: I want to prove that it gives me my OriginalShip object when I ask for it. Within my newly renamed test class, I rename the default test method from TestMethod to GetOriginalShippingMethodTest. Because I’ve already generated my costing objects, my first test looks like this:

[TestMethod]
public void GetOriginalShippingMethodTest()
{
   ShippingMethodFactory smf = new ShippingMethodFactory();

   IShippingCosting sStrat = smf.GetShippingMethod(ShippingMethod.USPS);

   Assert.IsNotNull(sStrat);
   Assert.IsInstanceOfType(sStrat, typeof(IShippingCosting));
   Assert.IsInstanceOfType(sStrat, typeof(USPSShip));
}

If my shipping method required any special configuration, I’d expose those options through read-only properties in the IShippingCosting interface and check them in this test, also.

And (surprisingly for me) my first test passes—the benefit of leveraging Visual Studio and cutting/pasting existing code when I did my original refactoring. Depending on my level of paranoia, I might create additional tests for each of my costing objects (if I were using XUnit instead of MSTest, the ClassData attribute would make that easy).

Testing the Costing Object

Now I’m ready to test my OriginalShipping class. I’d prefer my new test to be as isolated as possible from any other classes—that way I know if anything goes wrong, it’s the fault of OriginalShip object.

To make the problem more interesting, the product that’s passed to my ShipProduct method (and that I pass on to my CalcMethodCost) is just an interface—I don’t actually know what class is used. I could use the ProductRepo class that returns Product objects from the database to give me a sample IProduct object … but that wouldn’t give me the isolation I want: Any change to the database could derail my test.

This isn’t an unusual scenario and is exactly the reason that mocking tools (like JustMock) exist. I can use JustMock to ensure that I always get the IProduct objects I want for my test—even when all I have is an interface.

The start of my test creates a mock object for the IProduct interface that specifies the values for various properties that I want for my test. To further isolate my test, I also create a mock object to return the shipping factory I want for the test:

[TestMethod]
public void OriginalShipProduct1Test()
{
   IProduct prodMock = Mock.Create<IProduct>();
   Mock.Arrange(() => prodMock.ProdId).Returns("A123");
   Mock.Arrange(() => prodMock.weight).Returns(200);
   

   ShippingMethodFactory sf = new ShippingMethodFactory();
   Mock.Arrange(() => sf.GetShippingMethod(ShippingMethod.Original))
                                           .Returns(new OriginalShip());
   IShippingCosting os = sf.GetShippingMethod(ShippingMethod.Original);

   decimal res = os.CalcMethodCost(prod, ShippingUrgency.High, .13m);

   Assert.AreEqual(145.5m, res); 
}        

Now, it’s just a matter of replicating this test for every case relevant to OriginalShip. After that, I repeat the process as I build out the other costing objects.

Looking ahead to those other tests, it makes sense to me to refactor my test code to save some time later on. I cut the JustMock code from my first test and create a new test “helper” method that will generate IProduct objects on demand:

private IProduct createMockProduct(string Id, 
                  			          int Weight,
         …more parameters relevant to testing shipping costs)
        {
            IProduct mockProduct = Mock.Create<IProduct>();
            Mock.Arrange(() => mockProduct.prodId).Returns(Id);
            Mock.Arrange(() => mockProduct.weight).Returns(Weight);
            //…more properties, some hard-coded for default value…

            return mockProduct;
        }

Now my tests can just begin with a line like this:

IProduct mockedProduct = CreateMockProduct("B456", 10);

Third Step: Writing the Component-Level Tests

Once I’ve written enough unit tests to convince myself that everything is working correctly with my costing objects, I’ll move up to the component level. Here, I want tests for my new class that prove all my code works together to deliver the answer the business expects.

That test is pretty short and uses just one mock object before calling my shipping cost calculator:

[TestMethod]
public void ShippingCostCalculatorTest()
{
   IProduct mockedProduct = CreateMockProduct("C789", 1000);

   ShippingCostCalculator scc = new ShippingCostCalculator();
   decimal res = scc.CalcShipping(prod, 200, ShippingUrgency.High, ShippingMethod.Original, .13m);

   Assert.AreEqual(180m, res);
}

Again, depending on my level of paranoia, I’ll write additional tests for different costing/product object combinations.

Looking Back

The business wanted to extend the application to handle multiple shipping methods, and I wanted to do it in a way that would keep maintenance costs down and let me automate my regression testing. An hour to an hour and a half of refactoring let me achieve all those goals.

I will have written a lot of test code along the way … but you can’t avoid testing: You can either automate it or do it manually on every release.

Automating these tests gives me a very powerful regression testing suite. After I write these tests, if I make any change to a costing object, I can run my unit-level tests for that costing object to make sure it still works correctly. If I ever make any changes to CalcShipCost, I can rerun my component-level tests to make sure that CalcShipCost still works correctly.

In fact, for any change to any of these classes, these automated tests will prove all of the test scenarios still work correctly. That eliminates a ton of manual regression testing. And, when I run all my automated regression tests in less than a minute, everyone else will figure it was code worth writing, too.


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.