Write Testable Code
How and Why?

Why writing tests?

Tests reduce bugs in new features

  • Force developers to think twice about their code
  • Edge cases are more likely to be tested
  • Consequence: improved robustness

Tests reduce bugs in existing features

  • Existing tests will fail while adding new features
  • Without tests, new bugs may take days or weeks to be discovered

Tests are good documentation

  • A concise code example is better than many paragraphs of documentation
  • Need to learn a new API? Look at the existing tests!

Tests reduce the cost of change

  • Developers have confidence that changes do not break existing functionality
  • More time can be dedicated to coding, instead of manual testing
  • Poorly-tested software becomes increasingly expensive to change as time goes on (manual testing becomes very painful and bugs comes more often)

Tests improve design

  • Writing tests forces you to make your code testable
  • Reduces coupling in your application, as well as dubious patterns like singletons and global variables

Tests helps refactoring

  • Without tests, refactoring is pretty scary
  • Refactoring is a necessity to improve a software among the years

Tests defend against other devs

  • A good test is more valuable than 10 lines of comment
  • Programmers constantly write weird code to fix weird bugs... tests can help you make sure no one will delete that code thinking it's a mistake!

Many other reason

  • Tests reduce fear when changing the code of an employee / intern that left the company
  • Testing is fun and challenging!
  • Tests make development faster on the long run

What types of tests?

Unit Tests

  • Repeatable: you can rerun the same test as many times as you want
  • Consistent: every time you run it, you get the same result
  • In memory: it has no "hard" dependencies on anything not in memory (such as file system, databases, network)
  • Fast: It should take less than half a second to run
  • Check one single concern or "use case" in the system

Integration tests

  • Reach out to external systems or local machine dependencies (web services, database, files)
  • Test multiple things in the course of one test case
  • Are usually much slower than unit tests
  • Are more likely to fail randomly

So then why integration tests?

  • Extra "smoke test" that can be run when the full system needs to be tested
  • It is usually hard or impossible to write unit tests for legacy code (i.e. code that does not have tests), as it is mostly untestable

So, how do I write testable code?

Rule #1: Avoid constructors that do too much

  • New keyword in a constructor or at field declaration
  • Static method calls in a constructor or at field declaration
  • Anything more than field assignment in constructors
  • Object not fully initialized after the constructor finishes (watch out for 'initialize' methods)
  • Complex object graph construction inside a 
    constructor rather than using a factory or builder
  • Conditional or looping logic in a constructor

Why is that an issue?

  • Tight coupling
  • Prevents reusing class with different setup
  • Prevents injection of dependencies for testing

Use “new” keyword in constructor (or field declaration)

public class House 
{
  private Kitchen kitchen = new Kitchen();
  private Bedroom bedroom;

  public House() 
  {
    this.bedroom = new Bedroom();
  } 
}
public class House 
{
  private Kitchen kitchen;
  private Bedroom bedroom;

  @Inject
  public House(Kitchen k, Bedroom b) 
  {
    this.kitchen = k;
    this.bedroom = b;
  }
}
public void TestThisIsReallyHard() 
{
  House house = new House();
  // Darn! I'm stuck with those Kitchen and
  // Bedroom objects created in the
  // constructor...
}
public void testThisIsEasyAndFlexible() 
{
    Kitchen dummyKitchen = new DummyKitchen();
    Bedroom dummyBedroom = new DummyBedroom();

    House house = 
        new House(dummyKitchen, dummyBedroom);
    // Awesome, I can use test doubles that
    // are lighter weight...
}

Network calls / static calls in constructor (WTF!)

public class AccountView 
{
  private User user;
  
  public AccountView() 
  {
    // Static methods are non-interceptable, 
    // and non-mockable
    this.user = DatabaseManager.GetCurrentUser();
  }
}
public class AccountView 
{
  private User user;
  
  public AccountView(User user) 
  {
    this.user = user;
  }
}
// Hard to test because needs real database
public void TestWithRealDatabase() 
{
  AccountView view = new AccountView();
  // Dammit! We just had to connect to a
  // real database. This test is now slow.
}
// Easy to test with dependency injection
public void TestLightweightAndFlexible() 
{
  User user = new DummyUser();
  AccountView view = new AccountView(user);
  // Easy to test and fast with test-double user
}

Rule #2: Avoid digging into collaborators

  • Objects are passed in but never used directly (only used to get access to other objects)
  • Suspicious names: context, manager, helper, ...

Service object digging around in value objects

public class TaxCalculator {
  private TaxTable taxTable;
  public SalesTaxCalculator(TaxTable taxTable) {
    this.taxTable = taxTable;
  }

  float ComputeSalesTax(User user, Invoice invoice) {
    // Note that "user" is never used directly
    Address address = user.Address;
    float amount = invoice.SubTotal;
    return amount * taxTable.GetTaxRate(address);
  }
}
public class TaxCalculator {
  private TaxTable taxTable;
  public SalesTaxCalculator(TaxTable taxTable) {
    this.taxTable = taxTable;
  }

  float ComputeSalesTax(Address address, float amount) 
  {
    // No user, nor digging inside the invoice
    return amount * taxTable.GetTaxRate(address);
  }
}
void PainfulTaxCalculatorTest() 
{
  TaxCalculator calc = new TaxCalculator(new TaxTable());
  
  // Testing exposes the problem by the amount 
  // of work necessary to build the object graph
  Address address = new Address("1600 Amphitheatre Parkway...");
  User user = new User(address);
  Invoice invoice =  new Invoice(1, new ProductX(95.00));

  Assert.AreEqual(0.09, 
    calc.ComputeSalesTax(user, invoice));
}
void QuickTaxCalculatorTest() 
{
  TaxCalculator calc = new TaxCalculator(new TaxTable());

  // Only wire together the objects that are needed
  Address address = new Address("1600 Amphitheatre Parkway...");
  
  Assert.AreEqual(0.09, 
    calc.ComputeSalesTax(address, 95.00));
}

Use of "context" object containers

public class MembershipPlan
{
  public void ProcessOrder(UserContext userContext) 
  {
    User user = userContext.User;
    PlanLevel level = userContext.Level;
    Order order = userContext.Order;
    
    // Do stuff
  }
}
public class MembershipPlan 
{
   public void ProcessOrder(User user, 
     PlanLevel level, Order order) 
   { 
     // Do stuff
   }
}
public void TestWithContext() 
{
  UserContext userContext = new UserContext
  {
    User = new User("Kim"),
    Order = new Order("SuperDeluxe", 100, true),
    Level = new PlanLevel(143, "yearly")
  }
  MembershipPlan plan = new MembershipPlan();

  plan.processOrder(userContext);
  // Then make assertions against the user, etc ...
}
public void TestWithApiDeclaringWhatItNeeds() 
{
  User user = new User("Kim");
  PlanLevel level = new PlanLevel(143, "yearly");
  Order order = new Order("SuperDeluxe", 100, true);

  MembershipPlan plan = new MembershipPlan();

  plan.processOrder(user, level, order);
  // Then make assertions against the user, etc ...
}

Rule #3: Static code is the enemy of automated testing!

  • Cheap and immediately available (no need to call 'new')
  • Can be called from anywhere
  • Good for Simple calculations / processing (ex: Math.Abs())
  • Unique implementation: no polymorphism!
  • Life time = application (no construction, no destruction) 
  • Memory usage: no garbage collection
  • Static variable = global state
  • Not object-oriented, definitely procedural
  • Encourage tight coupling
  • Almost impossible to mock in C#
  • Make APIs lie about their true dependencies

Why can't I unit test this code?

/// <summary>
/// Checks if a mailbox has any error.
/// </summary>
/// <param name="mailbox">The mailbox.</param>
/// <returns>True if has errors.</returns>
public static bool HasErrors(Mailbox mailbox)
{
    // Data validation
    Errors.Assert(null != mailbox);
    Errors.Assert(Guid.Empty != mailbox.Id);

    // Get the ticket
    Ticket ticket = new Ticket(WebAuthUtils.User.Id, false);

    // Get migration stats
    MailboxStat mailboxStat = Mailbox.GetMigrationStat(mailbox.Id);

    // Return true if there are too many errors: more than 20 and more than 3%
    const int minErrors = 20;
    const double minErrorPercentage = 0.03;
    return (minErrors < mailboxStat.TotalErrorCount) 
        && (minErrorPercentage * mailboxStat.TotalErrorCount < mailboxStat.TotalErrorCount);
}

Here is a more testable version

public class TheSurroundingClass
{
    public IDataManager DataManager { get; set; }

    public TheSurroundingClass(IDataManager dataManager) 
    {
        this.DataManager = dataManager;
    }

    public bool HasErrors(Mailbox mailbox)
    {
        // Data validation
        Errors.Assert(null != mailbox);
        Errors.Assert(Guid.Empty != mailbox.Id);
    
        // Get the ticket
        Ticket ticket = new Ticket(WebAuthUtils.User.Id, false);
    
        // Get migration stats
        MailboxStat mailboxStat = this.DataManager.Find("MailboxStat", mailbox.Id);
    
        // etc.
    }
}

And now what? 

public interface IDataManager
{
    public Entity Find(string entityName, Guid id);
}

public class SqlDataManager
{
    public Entity Find(string entityName, Guid id)
    {
        // Do some SQL crap
    }
}

public class FakeDataManager
{
    public Entity Find(string entityName, Guid id)
    {
        // Return in-memory fake object
    }
}

We can implement a fake data manager!

And now what?

// Create a new Data Manager mock
Mock<IDataManager> dataManagerMock = new Mock<IDataManager>();

// Create a fake mailbox stat object
MailboxStat mailboxStat = new MailboxStat() { Speed = 12, Foo = "Bar" };

// Setup the mock to return fake mailbox stat (version 1)
dataManagerMock.Setup(p => p.Find(It.IsAny<string>(), It.IsAny<Guid>())).Returns(mailboxStat);

// Setup the mock to return fake mailbox stat (version 2)
dataManagerMock.Setup(p => p.Find("MailboxStat", It.IsAny<Guid>())).Returns(mailboxStat);

We can even mock the data manager!

public interface IDataManager
{
    public Entity Find(string entityName, Guid id);
}

public class SqlDataManager
{
    public Entity Find(string entityName, Guid id)
    {
        // Do some SQL crap
    }
}

Tight coupling: example 1

public class FooContext : IFooContext
{
    private FooContextState state;
    private ReaderWriterLockSlim theLock;
    private FooContextDescriptor internalObject;
    private ReaderWriterLockSlim anotherLock;
    public BarManagement BarManager { get; private set; }
    public OtherManagement OtherManager { get; private set; }

    internal FooContext(FooContextType fooType, Guid userId)
    {
        this.internalObject = FooContextUtility.CreateFooDescriptor(fooType, userId);
        this.Initialize();
    }

    private void Initialize()
    {
        this.theLock = new ReaderWriterLockSlim();
        this.BarManager = new BarManagement(this);
        this.OtherManager = new OtherManagement(this);
        this.PlopManager = new PlopManagement(this);
        this.EvenMoreManager = new EvenMoreManagement(this);
        this.anotherLock = new ReaderWriterLockSlim();
        this.state = this.internalObject.State;
    }
}

Tight coupling: example 2

// Hard to test
public static long DetermineSomething(Guid licensePackId)
{
    // Get the license pack
    LicensePack licensePack = LicensePack.Retrieve(licensePackId);
    Errors.AssertNotNull(licensePack);

    // Return 0 if blah
    if (licensePack.Blah)
        return 0;

    // Get invoice
    Guid invoiceId = licensePack.InvoiceId;
    Invoice invoice = Invoice.Retrieve(invoiceId);

    // Do more stuff
}

// Easier to test
public static long DetermineSomething(LicensePack licensePack, Invoice invoice)
{
    // Return 0 if blah
    if (licensePack.Blah)
        return 0;

    // Do more stuff
}

Tight coupling: example 3

public MigrationManager(int maxApiCallRetryCount = 2)
{
    // Set members
    this.WebService = new WebService 
    {
        Url = ConfigurationManager.Read<string>("WebApiUrl").Value 
    };
    this.MaxApiCallRetryCount = maxApiCallRetryCount;
}

Real Life Example

The most painful test I ever wrote

Goal: Test the refund REST service endpoint

  • Integration test
  • Already tested manually and working
  • Requires a lot of dependencies

Text

Text

Refund Credit Card - Call Graph

Assumptions

  • Database available (several)
  • Network available
  • File system available (config file, email templates)

Issues

  • Lot of static classes
  • No visibility on the dependencies
  • Impossible to mock / change implementation
  • Impossible to unit test
  • Test are slow and can easily break:
    • Several database calls / network calls
    • Many file access
    • Used to require an SMTP server!

Solution(-ish)

  • Bootstrap MigrationWiz and Lookup databases
  • Create a test organization and a test user
  • Authenticate that user
  • Create a test invoice and a test license pack
  • Switch Authorize.Net client in test mode
  • Mock the mailer to avoid file access
  • Make sure emails are not being sent

Possible improvements

  • Inject dependencies:
    • Pass them in constructor
    • Use factories
    • Use dependency injection framework
  • Stop using a database:
    • Switch to an in-memory database (fancy)
    • Implement / mock a fake DataAccessManager
  • Stop calling Authorize.Net (they have their own tests!)

What can we do?

  • Use static only when it makes sense
  • Avoid "hardcoding" dependencies: pass them instead!
  • De-staticify core classes (DataAccessManager, LicenseManager, ...)
  • Deploy a continuous integration platform
  • Add testing into our development practices
  • Assess testability during code reviews

Sources

Thanks!

Write Testable Code

By Guillaume Zurbach

Write Testable Code

  • 833