Unit Testing

Isolating the domain complexity from rest of the application allows us to tackle the complexity of the problem at hand without worrying about the technical infrastructure which supports it. Domain model and service layer are two building blocks where this is implemented. In applications where these blocks play a vital role, unit tests for them also become extremely important. In this chapter lets look at what challenges we might face and some techniques which are useful.

Testing entities/aggregates

Since by design anything which is part of domain model is isolated from technical infrastructure, they lends themselves well for testing, as they do not make call to the database, external service, and so on. In a poorly design domain layer this might be significant hurdle, but readers familiar with DDD concepts know how to get around this problem we would not get into the details of it. Given this lets look at a sample test.

When creating a new loan the terms of loan should be validated against the terms of product offering. This check is implemented in the method:

ValidationErrors verifyTerms(LoanTerms loanTerms) {
...
}

The test for which is:

void verifyTerms_() {

LoanProductFees loanProductFees = new LoanProductFees();

LoanProductFee loanProductFee = new LoanProductFee(LocalMoney.of(10), LocalMoney.of(20));

loanProductFee.id(1);

loanProductFees.addAll(Arrays.asList(loanProductFee));

LoanProduct loanProduct = new LoanProduct(new MoneyRange(100, 10000), new DoubleRange(10, 20), new DoubleRange(12, 24), new LocalDate(2011, 1, 1),

loanProductFees);

RequestedLoanFees requestedLoanFees = new RequestedLoanFees();

requestedLoanFees.addAll(Arrays.asList(new NewLoanRequest.RequestedLoanFee(1, LocalMoney.of(10))));

LoanTerms loanTerms = new LoanTerms(LocalMoney.of(100000), 15, 15, new LocalDate(2011, 2, 1), requestedLoanFees);

ValidationErrors errors = loanProduct.verifyTerms(loanTerms);

Assert.assertEquals(errors.size(), 1);

//more asserts

}

In order to test verifyTerms method one needs to build LoanProduct and LoanTerms objects. Each of which would require other objects to be created. As a result:

  1. Language noise: While writing it all down isn't too much of an issue for the modern IDEs as are quite helpful here. But like any code, the test code is read lot more than written (if not more).
  2. Irrelevant domain details which don't have any role to play in testing but come into picture as constructors demand them.
  3. Context specific irrelevance of some domain details for the current test.
  4. Duplication of test code across different tests.
  5. Anonymous parameters make it hard to understand the meaning of the data. For example there are two double ranges but without using IDE shortcuts or opening LoanProduct constructor we don't know which one is which. This can be improved by using a variable but that would increase the language noise (1).

The technique quite commonly employed is to create factory methods for construction of these objects, commonly known as Object Mother (http://martinfowler.com/bliki/ObjectMother.html). Object mothers help in 1, 2, 3 & 4 but the problem gets shifted from test to the mothers. One ends up with variety of methods in the mother, multiple tests dependent on same method making it hard to change them, essentially becoming a dumping ground for object construction. In most projects they never get re-factored as any change to it has impact on a lot of tests. A deeper understanding of the problem helps in coming up with some techniques.

Irrelevant domain details

As we have seen in chapter on Insight into Entity there are extrinsic attributes/associations and infrastructure related data do not play any the business logic. With this insight we can avoid these fields when it comes to testing. Object mother takes care of it for testing but if we go one step further and omit them from the constructor then we make this explicit to all clients and not only the tests. For example as we see in the sample code above we have not used defaultAmount, defaultInterestRate, defaultNumberOfInstallments and fundName in the constructor of the LoanProduct as its constructor doesn't demand this.

Language noise and anonymous parameters

import static petmongrels.sdb.product.ProductAttributes.*;
import static petmongrels.sdb.product.ProductFeeAttributes.*;
import static petmongrels.sdb.utility.PrimitivesFactory.*;
...
...
void verifyTerms() {
        LoanProduct loanProduct = new LoanProduct(amountRange(100, 10000), interestRange(10, 20), numberOfInstallmentsRange(12, 24), effectiveFrom(2011, 1, 1),
                                                  productFees(productFee(id(1), minimum(10), maximum(20))));
        LoanTerms loanTerms = new LoanTerms(amount(100000), interest(15), numberOfInstallments(15), date(1, 2, 2011), fees(fee(id(1), amount(10))));
        ValidationErrors errors = loanProduct.verifyTerms(loanTerms);
        assertEquals(errors.size(), 1);
    }

In this code sample we try make the test more readable by using methods, created for tests, statically imported. Although the use of static imports is discouraged, for valid reasons in production code, but we can use it to our advantage in the test code. (In languages like C# extension methods on Object class can be used to get the same affect). In Java we have Attribute classes in the test code which define these methods.

class LoanProductAttributes {

public static DoubleRange numberOfInstallmentsRange(int minimum, int maximum) {

return new DoubleRange(minimum, maximum);

}

public static DoubleRange interestRange(int minimum, int maximum) {

return new DoubleRange(minimum, maximum);

}

public static MoneyRange amountRange(int minimum, int maximum) {

return new MoneyRange(minimum, maximum);

}

public static double interest(int rate) {

return rate;

}

public static double interest(double rate) {

return rate;

}

}

This can be done by defining these methods in a single class and making it the base class for actual test class. This, base test class approach, would lead to a really large class and would lead duplication or need for multiple-inheritance when there are multiple domain modules in the application. With attribute classes these methods can be spread out meaningfully in smaller and specifically named classes. (Also, I am using TestNG and have used assertEquals instead of using Assert.assertEquals everytime, following the same idea)

Another handy thing with attribute classes is that they can take care of converting int to short, decimal, double etc, not requiring us to type cast them in the test. For specific cases we can always have overloaded methods, e.g. interest method in LoanProductAttributes class.

Before we look at mechanism for handling the issue of context specific irrelevance and duplication of test code lets look at some tests for LoanAccount class.

public void accountStatusOnDisbursal() {
    LoanAccount loanAccount = LoanAccount.newLoan(amount(1000), interest(10), numberOfInstallments(12), loanFees(recurringLoanFee(amount(10))));
    loanAccount.disburse();
    assertEquals(LoanAccountStatus.DISBURSED, loanAccount.status());
}
public void disburseLoanWithOneTimeAndRecurringFee() {
    LoanAccount loanAccount = LoanAccount.newLoan(amount(1000), interest(10), numberOfInstallments(12), loanFees(oneTimeLoanFee(amount(10)), recurringLoanFee(amount(10))));
    loanAccount.disburse();
    //asserts payment schedule
}
public void accountStatusOnCancellation() {
    LoanAccount loanAccount = LoanAccount.newLoan(amount(1000), interest(10), numberOfInstallments(12), loanFees(recurringLoanFee(amount(10))));
    loanAccount.cancel();
    assertEquals(LoanAccountStatus.CANCELLED, loanAccount.status());
}
@ExpectedExceptions(InvalidLoanStateException.class)
public void cancelADisbursedLoan() {
    LoanAccount loanAccount = LoanAccount.newLoan(amount(1000), interest(10), numberOfInstallments(12), loanFees(recurringLoanFee(amount(10))));
    loanAccount.disburse();
    loanAccount.cancel();
}
public void missAnInstallment() {
    LoanAccount loanAccount = LoanAccount.newLoan(amount(1000), interest(10), numberOfInstallments(12), loanFees(recurringLoanFee(amount(10))));
    loanAccount.disburse();
    loanAccount.installmentMissed();
    assertEquals(LoanAccountStatus.IN_BAD_STANDING, loanAccount.status());
}

The fact that ever test is constructing a loan account is a duplication. There are three standard ways to handle this.

Setup method: While using setup method is simplest thing, we should not use it here as disburseLoanWithOneTimeAndRecurringFee test needs a different setup from other tests. We should avoid hacking this by reconstructing the loan account in the test and changing the instance created in the setup.

private LoanAccount loanAccount;
public void setup() {
    loanAccount = LoanAccount.newLoan(amount(1000), interest(10), numberOfInstallments(12), loanFees(recurringLoanFee(amount(10))));
}
public void accountStatusOnDisbursal() {
    loanAccount.disburse();
    assertEquals(LoanAccountStatus.DISBURSED, loanAccount.status());
}
public void disburseLoanWithOneTimeAndRecurringFee() {
    loanAccount = LoanAccount.newLoan(amount(1000), interest(10), numberOfInstallments(12), loanFees(oneTimeLoanFee(amount(10)), recurringLoanFee(amount(10))));
    loanAccount.disburse();
    //asserts payment schedule
}
public void accountStatusOnCancellation() {
    loanAccount.cancel();
    assertEquals(LoanAccountStatus.CANCELLED, loanAccount.status());
}

Local method: A local method to construct the loan account allows one to parameterize the construction of loan account. But this doesn't allow one to use this method from other test classes.

Object mother pattern: It allows different tests to construct loan accounts. This pattern is well known would want to stress that we would use still use domain insight, cancel language noise, etc as discussed so far even when we are using this pattern. Lets look at the example below:

public void accountStatusOnDisbursal() {

LoanAccount loanAccount = disbursedLoan();

assertEquals(LoanAccountStatus.DISBURSED, loanAccount.status());

}

public void disburseLoanWithOneTimeAndRecurringFee() {

LoanAccount loanAccount = simpleLoan(loanFees(oneTimeLoanFee(amount(10)), recurringLoanFee(amount(10))));

loanAccount.disburse();

//asserts payment schedule

}

public void accountStatusOnCancellation() {

LoanAccount loanAccount = simpleLoan();

loanAccount.cancel();

assertEquals(LoanAccountStatus.CANCELLED, loanAccount.status());

}

@ExpectedExceptions(InvalidLoanStateException.class)

public void cancelADisbursedLoan() {

LoanAccount loanAccount = disbursedLoan();

loanAccount.cancel();

}

public void missAnInstallment() {

LoanAccount loanAccount = disbursedLoan();

loanAccount.installmentMissed();

assertEquals(LoanAccountStatus.IN_BAD_STANDING, loanAccount.status());

}

LoanAccountMother class

public static LoanAccount simpleLoan() {

return simpleLoan(loanFees(recurringLoanFee(amount(10))));

}

public static LoanAccount simpleLoan(LoanFees loanFees) {

return LoanAccount.newLoan(amount(1000), interest(10), numberOfInstallments(12), loanFees);

}

public static LoanAccount disbursedLoan() {

LoanAccount loanAccount = simpleLoan();

loanAccount.disburse();

return loanAccount;

}

While the object mother pattern is quite simple but it needs as much attention as other parts of your code base. In most code bases I have worked on it becomes quite problematic. Lets see what causes it and how can we get around it.

Entity construction in invalid states

If some mother methods create entities in such states it implies that such objects are specific to certain tests. Other tests may not be able to use the same methods. In such case one needs to add new mother methods. With this one soon ends up with a lot methods whose names cannot be expressed in way which is relevant to the domain. If the entities can be constructed only in valid state (enforced via constructors) and doing state transitions, one can map the tests to these real world scenarios. (example: LoanAccountMother.disbursedLoan method)

Separate entity and object mothers

In insight into entity we saw that some fields in an entity may not have any role to play in domain logic, hence are not important to us when unit testing such code. Whereas when we test mapping of a entity to database and data transfer objects, one might want to test whether all the fields are mapped correctly. Object mother pattern naturally should be used for constructing such entities as well. When we use same mother methods for it we clutter it with unnecessary details. We should differentiate between entity mother and object mother. LoanAccountMother would be an example entity mother. Object mother can use entity mothers for convenience, since object mothers would populate super-set of data in the entity.

public static LoanAccount aDisbursedLoan() {
    LoanAccount loanAccount = LoanAccountMother.disbursedLoan();
    return populateExtrinsicFields(loanAccount);
}
private static LoanAccount populateExtrinsicFields(LoanAccount loanAccount) {
    loanAccount.number("IL0001").purposeOfLoan("Agriculture").notes("for seeds");
    return loanAccount;
}
public static LoanAccount aLoan() {
    LoanAccount loanAccount = LoanAccountMother.simpleLoan();
    return populateExtrinsicFields(loanAccount);
}

Using domain vocabulary for test objects

One of the reason domain driven design succeeds in handling complexity is because it maps the domain complexity to the code. The same principle can be applied to construction of test objects. The reason we write test it to verify a scenario which happens in the real world. Using domain vocabulary is a good idea for writing code as well as the tests. A shared vocabulary (of knowledge) goes a long way in preparing a developer for understanding test code. A developer should be able to explain what an mother method name would do without looking at the code.

Simple tips

When constructing data in the test use the simplest construction mechanism for the dependent objects. int for money.

Test name need not be summary of the test rather a scenario.

Static import approach forces you provide different name for test objects.