The Artima Developer Community
Sponsored Link

.NET Buzz Forum
Making your tests withstand design and interface changes - remove code duplication

0 replies on 1 page.

Welcome Guest
  Sign In

Go back to the topic listing  Back to Topic List Click to reply to this topic  Reply to this Topic Click to search messages in this forum  Search Forum Click for a threaded view of the topic  Threaded View   
Previous Topic   Next Topic
Flat View: This topic has 0 replies on 1 page
Roy Osherove

Posts: 1807
Nickname: royo
Registered: Sep, 2003

Roy Osherove is a .Net consultant based in Israel
Making your tests withstand design and interface changes - remove code duplication Posted: Apr 11, 2005 8:58 AM
Reply to this message Reply

This post originated from an RSS feed registered with .NET Buzz by Roy Osherove.
Original Post: Making your tests withstand design and interface changes - remove code duplication
Feed Title: ISerializable
Feed URL: http://www.asp.net/err404.htm?aspxerrorpath=/rosherove/Rss.aspx
Feed Description: Roy Osherove's persistent thoughts
Latest .NET Buzz Posts
Latest .NET Buzz Posts by Roy Osherove
Latest Posts From ISerializable

Advertisement
One of the biggest pitfalls developers writing unit tests face is in the face of a re-design. They write hundreds of unit tests for various classes, and suddenly a design change breaks many of them - a default constructor is removed, parameters are added or removed from interfaces, various default conditions change - havoc rears its ugly head.
The poor developer is then faced with "fixing" hundreds of tests just because a method was added to a constructor.
 
To remove this obstacle from the developer one must take the precaution in writing the tests. There are several rules you should abide by that will save your hiny on most events where such a change occurs:
here are some guidelines:
 
  • Encapsulate object creation code in helper methods inside your test fixture. If just your fixture is creating these objects - these methods should reside as private methods on the fixture. If more classes use them, they should be refactored out into a separate helper class with static helper methods for this creation (also known as the "Object Mother" pattern).
 
   for example:
      [Test]
      public void Sum_ReturnsZeroByDefault()
      {
         Calculator calc = new Calculator();
         int result = calc.Sum();
         ...
      }
 
should be refactored into:
 
        [Test]
      public void Sum_ReturnsZeroByDefault()
      {
         Calculator calc = setup_CreateCalculator();
         int result = calc.Sum();
         ...
      }
 
      private Calcualtor setup_CreateCalcualtor()
      {
         return new Calculator();
      }
 
notice the following:
    • After this change, any changes to the Calculator's public constructor should only propagate to a small "fix" in one private method in the fixture, used by many tests.
    • The helper method has a special prefix "setup_" to denote that it is a helper method used to setup object instances for test cases. It also makes your test more readable.
If multiple test fixture need to use the Calculator object, the code might turn into:
 
  [Test]
      public void Sum_ReturnsZeroByDefault()
      {
         Calculator calc = setup_CreateCalculator();
         int result = calc.Sum();
         ...
      }
 
      private Calcualtor setup_CreateCalcualtor()
      {
         return CalctulatorTestHelper.CreateDefaultCalculator();
      }
 
         public class CalctulatorTestHelper
      {
               public static Calculator CreateDefaultCalculator()
         {
                     return new Calculator();
         }
      }
 
notice the following:
    • We still keep the original setup_ helper method so that we change as little code as we need to in the old fixture. We just delegate the work into the CalculatorHelperobject. This level of indirection might not be needed but might help us in the future if we find we need to actually make a change to the CalculatorHelper class.
    • None of this code sits in production, but is only related to the testing project
 
  • Encapsulate complex or lengthy object initialization code  or complex interaction code into helper methods in your fixture or helper objects. If you find that you have two tests that initialize an object's state to the same state - refactor that code into a different method that returns the object with the known state. This helps future changes to the various object methods and default state behavior not break your tests and you bursting into tears.
for example:
 
      [Test]
      [ExpectedException(typeof(Exception),"User cannot be added twice")
      public void AddUser_AddingSameUserTwiceThrowsException()
      {
         LoginManager lm = setup_CreateDefaultLoginManager();
                  lm.AddUser("a","b");
         //exception should be thrown here
                  lm.AddUser("a","");
      }
 
      [Test]
      public void ChangePassword_AddingSameUserTwiceThrowsException()
      {
         LoginManager lm = setup_CreateDefaultLoginManager();
                  lm.AddUser("a","b");
 
         lm.ChangePassword("a","c");
                  bool isLoginOk = lm.IsLoginOK("a","c");
                  Assert.IsTrue(isLoginOk,"Login should have been accepted for user with the new password"
      }
      private LoginManager setup_CreateDefaultLoginManager()
      {
         return new LoginManager();
      }
 
   should be replaced with something along the lines of:
 
      [Test]
      [ExpectedException(typeof(Exception),"User cannot be added twice")
      public void AddUser_AddingSameUserTwiceThrowsException()
      {
         LoginManager lm = setup_CreateLoginManagerIwhtOneDefaultUser()
         
         //exception should be thrown here
                  lm.AddUser("a","");
      }
 
      [Test]
      public void ChangePassword_AddingSameUserTwiceThrowsException()
      {
         LoginManager lm = setup_CreateLoginManagerIwhtOneDefaultUser
 
         lm.ChangePassword("a","c");
                  bool isLoginOk = lm.IsLoginOK("a","c");
                  Assert.IsTrue(isLoginOk,"Login should have been accepted for user with the new password"
      }
      
      private LoginManager setup_CreateLoginManagerIwhtOneDefaultUser()
      {
                  LoginManager lm = setup_CreateDefaultLoginManager();
                  lm.AddUser("a","b");
         return lm;
      }
 
some points on this:
    • You could also use various helper methods that take parameters (for example, a login manager with N users already added to it to check various counts
    • You should use this method whenever you find duplication in your tests. Don't do it until you see duplication in your test code. Your primary concern is to make the test work, then refactor the test code as well as the production code.
    • If you continue on with this method your tests will become very robust and be able to adept to new requirements in your code with little effort.
    • Note that this method helps when the design change does not affect the logic behind your test. For example, the method of adding a user to LoginManager might change, but Changing a password for the user (as depicted above) should still be tested the same. If the design affects your test logic - there's no escape but changing the test. You should be very wary of changing your tests just to make them pass. Sometimes they break because they are telling you that you broke something in your logic.

Read: Making your tests withstand design and interface changes - remove code duplication

Topic: Design question Previous Topic   Next Topic Topic: Default Instance Considered Harmful...?

Sponsored Links



Google
  Web Artima.com   

Copyright © 1996-2019 Artima, Inc. All Rights Reserved. - Privacy Policy - Terms of Use