How to Write Unit Test for
New Code on top of Legacy Code


Joseph Yao
Please Read This Great Book!
Unit Test 101
What’s Unit Test?

Unit tests is the idea that they are tests in
isolation of individual components of software


                            - Michael C. Feathers
What’s Legacy Code?
Legacy code is simply code without tests.


Without tests is bad code. It doesn't matter how well written
it is; it doesn't matter how pretty or object-oriented or well-
encapsulated it is. With tests, we can change the behavior
of our code quickly and verifiably. Without them, we really
don't know if our code is getting better or worse.


                                          - Michael C. Feathers
Make New Code Testable
How to Initialize Dependency




               Need



                      Legacy Code
  New Code
Your Code Looks Like This?
public class Car {
      private Engine engine;

      public Car() {
            engine = new Engine();
      }

      public void run() {
            engine.start();
      }

      public String status() {
            return engine.speed() > 0 ? “Move”: “Stop”;
      }
}
Your Test Looks Like This?
public class TestCar {
     @Test public void move() {
            Car car = new Car();
            car.run();
            assertEquals(“Move”, car.status());
     }
}


         Are you really do the unit test for Car?
Code for Car with Isolation
public class Car {
      private IEngine engine;

      public Car(IEngine theEngine) {
            engine = theEngine;
      }

      public void run() {
            engine.start();
      }

      public String status() {
            return engine.speed() > 0 ? “Move”: “Stop”;
      }
}
Test for Car with Isolation
public class TestCar {
      @Test public void move() {
            IEngine engineMock =
                  new EngineMock(10);
            Car car = new Car(engineMock);
            car.run();
            assertEquals(“Move”, car.status());
      }
}


In fact, you don’t care how engine speed is calculated
I Need to Test a Private Method
Your Code Looks Like This?
public class Order {
     public void addItem(Item item) {
           if (isValidItem(item)) {
                items.add(item);
           }
     }
     private boolean isValidItem(Item item) {
           more than 500 lines code…
     }
}


      How can I test the private method?
Does Your Class Have too many Responsibilities?
Maybe This Code is Better?
public class Order {
     private ItemValidator validator;
     public Order (ItemValidator
          theValidator) {
          validator = theValidator;
     }
     public void addItem (Item item) {
          if (validator.
               isValidItem(item)) {
               …
          }
     }
}
Single Responsibility Principle
“Don’t Do it” unless you have a Very Strong Reason




                                   +
Good design is testable, and
design that isn't testable is bad



              - Michael C. Feathers
Isolate Legacy Code
Too hard to get Legacy Code under Test
Some New Code needed to be added
 public class Customer {
      …
      public void purchase() {
               more than 500 lines of legacy code…
        }
        …
 }

 We need to log this customer purchase action after it done.

Can you add unit test for new code with no impact to legacy code?
Add new code by Sprout Method
public class Customer {
     public void purchase() {
           purchaseWithoutLog();
           logPurchaseAction();
     }
     protected void purchaseWithoutLog() {
           more than 500 lines of legacy code…
      }
      private void logPurchaseAction() {
           Your new code here…
      }
}
Your Test May Look Like This
public class TestCustomerPurchaseLog extends
     Customer {
     @Test public void log() {
           Customer customer =
                new TestCustomerPurchaseLog();
           customer.purchase();
           … code to verify the log action …
     }

     protected void purchaseWithoutLog() {}
}
Some other Alternative Ways

• Sprout Method – the Sample Code

• Wrap Method

• If the legacy class is hard to be put into test
  harness
  o Sprout Class

  o Wrap Class

  o This is quite useful when you can’t easily isolate the
    dependency for legacy class
I have a Monster Dependency to Isolate
How to Isolate “HttpServletRequest”?
public class ARMDispatcher {

    public void populate (HttpServletRequest request) {

        String [] values
               = request.getParameterValues(pageStateName);

        if (values != null && values.length   > 0) {

               marketBindings.put(

               pageStateName + getDateStamp(), values[0]);

        }

    }

}
You only Need to get the Parameter Value
public class ARMDispatcher {

    public void populate (ParameterSource source) {

        String value =

        source.getParameterForName(pageStateName);

        if (value != null) {

              marketBindings.put(

              ageStateName + getDateStamp(), value);

        }

    }

}
I need D, but it’s from A.getB().getC().getD()
Your Code may Look like this
public class Customer {
    private Orders orders;
    public List<String> getAllItemNamesOfLatestOrder(){
        List<String> allNames =
              new ArrayList<String>();
        Item[] items =
              orders.getLatestOrder().getAllItems();
        for (Item item : items) {
              allNames.add(item.getName());
        }
        return allNames;
    }
}
You need to Isolate the way to Get Items
public class Customer {
  private Orders orders;
  public List<String> getAllItemNamesOfLatestOrder(){
      List<String> allNames =
            new ArrayList<String>();
      Item[] items = getAllItemsOfLatestOrder();
      for (Item item : items) {
            allNames.add(item.getName());
      }
      return allNames;
  }
  protected Item[] getAllItemsOfLatestOrder() {
      return orders.getLatestOrder().getAllItems();
  }
}
Suggestions
Test Behavior
      instead of

Test Implementation
Dependency Isolation Tool is


         Evil
Q&A




     新浪微博 @姚若舟
      TDD Code Kata
@tudou.com/home/yaoruozhou

Scrum Gathering 2012 Shanghai_工程实践与技术卓越分会场:how to write unit test for new code based on legacy code(姚若舟)

  • 1.
    How to WriteUnit Test for New Code on top of Legacy Code Joseph Yao
  • 2.
    Please Read ThisGreat Book!
  • 3.
  • 4.
    What’s Unit Test? Unittests is the idea that they are tests in isolation of individual components of software - Michael C. Feathers
  • 5.
    What’s Legacy Code? Legacycode is simply code without tests. Without tests is bad code. It doesn't matter how well written it is; it doesn't matter how pretty or object-oriented or well- encapsulated it is. With tests, we can change the behavior of our code quickly and verifiably. Without them, we really don't know if our code is getting better or worse. - Michael C. Feathers
  • 6.
    Make New CodeTestable
  • 7.
    How to InitializeDependency Need Legacy Code New Code
  • 8.
    Your Code LooksLike This? public class Car { private Engine engine; public Car() { engine = new Engine(); } public void run() { engine.start(); } public String status() { return engine.speed() > 0 ? “Move”: “Stop”; } }
  • 9.
    Your Test LooksLike This? public class TestCar { @Test public void move() { Car car = new Car(); car.run(); assertEquals(“Move”, car.status()); } } Are you really do the unit test for Car?
  • 10.
    Code for Carwith Isolation public class Car { private IEngine engine; public Car(IEngine theEngine) { engine = theEngine; } public void run() { engine.start(); } public String status() { return engine.speed() > 0 ? “Move”: “Stop”; } }
  • 11.
    Test for Carwith Isolation public class TestCar { @Test public void move() { IEngine engineMock = new EngineMock(10); Car car = new Car(engineMock); car.run(); assertEquals(“Move”, car.status()); } } In fact, you don’t care how engine speed is calculated
  • 12.
    I Need toTest a Private Method
  • 13.
    Your Code LooksLike This? public class Order { public void addItem(Item item) { if (isValidItem(item)) { items.add(item); } } private boolean isValidItem(Item item) { more than 500 lines code… } } How can I test the private method?
  • 14.
    Does Your ClassHave too many Responsibilities?
  • 15.
    Maybe This Codeis Better? public class Order { private ItemValidator validator; public Order (ItemValidator theValidator) { validator = theValidator; } public void addItem (Item item) { if (validator. isValidItem(item)) { … } } }
  • 16.
  • 17.
    “Don’t Do it”unless you have a Very Strong Reason +
  • 18.
    Good design istestable, and design that isn't testable is bad - Michael C. Feathers
  • 19.
  • 20.
    Too hard toget Legacy Code under Test
  • 21.
    Some New Codeneeded to be added public class Customer { … public void purchase() { more than 500 lines of legacy code… } … } We need to log this customer purchase action after it done. Can you add unit test for new code with no impact to legacy code?
  • 22.
    Add new codeby Sprout Method public class Customer { public void purchase() { purchaseWithoutLog(); logPurchaseAction(); } protected void purchaseWithoutLog() { more than 500 lines of legacy code… } private void logPurchaseAction() { Your new code here… } }
  • 23.
    Your Test MayLook Like This public class TestCustomerPurchaseLog extends Customer { @Test public void log() { Customer customer = new TestCustomerPurchaseLog(); customer.purchase(); … code to verify the log action … } protected void purchaseWithoutLog() {} }
  • 24.
    Some other AlternativeWays • Sprout Method – the Sample Code • Wrap Method • If the legacy class is hard to be put into test harness o Sprout Class o Wrap Class o This is quite useful when you can’t easily isolate the dependency for legacy class
  • 25.
    I have aMonster Dependency to Isolate
  • 26.
    How to Isolate“HttpServletRequest”? public class ARMDispatcher { public void populate (HttpServletRequest request) { String [] values = request.getParameterValues(pageStateName); if (values != null && values.length > 0) { marketBindings.put( pageStateName + getDateStamp(), values[0]); } } }
  • 27.
    You only Needto get the Parameter Value public class ARMDispatcher { public void populate (ParameterSource source) { String value = source.getParameterForName(pageStateName); if (value != null) { marketBindings.put( ageStateName + getDateStamp(), value); } } }
  • 28.
    I need D,but it’s from A.getB().getC().getD()
  • 29.
    Your Code mayLook like this public class Customer { private Orders orders; public List<String> getAllItemNamesOfLatestOrder(){ List<String> allNames = new ArrayList<String>(); Item[] items = orders.getLatestOrder().getAllItems(); for (Item item : items) { allNames.add(item.getName()); } return allNames; } }
  • 30.
    You need toIsolate the way to Get Items public class Customer { private Orders orders; public List<String> getAllItemNamesOfLatestOrder(){ List<String> allNames = new ArrayList<String>(); Item[] items = getAllItemsOfLatestOrder(); for (Item item : items) { allNames.add(item.getName()); } return allNames; } protected Item[] getAllItemsOfLatestOrder() { return orders.getLatestOrder().getAllItems(); } }
  • 31.
  • 32.
    Test Behavior instead of Test Implementation
  • 33.
  • 34.
    Q&A 新浪微博 @姚若舟 TDD Code Kata @tudou.com/home/yaoruozhou