Custom fluent assertions with AssertJ

There is no doubt in modern IT industry that writing tests is an integral part of software development. Untested code is like a black box. We do not know how it will behave in the production environment. And thus leaving a piece of logic without testing puts us in danger of introducing hard to track bugs into the system.

Therefore, almost every day at work each of us writes some tests. It does not matter if they are unit or integration tests — it is just our bread and butter. And yet, more often than not, developers put much less attention to the test code than to the production code. Libraries and frameworks, fluent APIs or just clean code in general are terms we usually connect with actual production code and not with tests. But investing some time to use them in test code can pay off very quickly.

In the article I will show you how we can introduce custom fluent assertions into our code base to make our tests easier to write, read and maintain. This approach might be an overkill for some cases, but I highly recommend to use it for critical parts of the system, such as core domain objects. It helps to avoid code repetition and allows to write a lot of high-quality tests faster.

Domain

Today our goal will be to check invoices generated by the service listed below:

public interface InvoiceGenerator {

	Invoice generateInvoiceForOrder(String orderId);

}

We would like to write a test for InvoiceGenerator and investigate returned Invoice object, making sure that it contains data we expect it to have. The invoice domain object itself is implemented as follows:

public class Invoice {

	private final DocumentInfo documentInfo;
	private final Contractor buyer;
	private final Contractor supplier;
	private final List<InvoiceItem> items;
	private final Currency currency;

	// all args constructor omitted for brevity

	public BigDecimal getTotalNetValue() {
		return items.stream()
				.map(InvoiceItem::getTotalNetValue)
				.reduce(BigDecimal.ZERO, BigDecimal::add);
	}

	public BigDecimal getTotalGrossValue() {
		return items.stream()
				.map(InvoiceItem::getTotalGrossValue)
				.reduce(BigDecimal.ZERO, BigDecimal::add);
	}

	public BigDecimal getTotalVatValue() {
		return items.stream()
				.map(InvoiceItem::getTotalVatValue)
				.reduce(BigDecimal.ZERO, BigDecimal::add);
	}

	// getters omitted for brevity

}

The DocumentInfo class is just a simple POJO holding references to invoice number (String), issuance date and due date (LocalDates).

Contractor is an abstract class representing either buyer or seller in the invoice:

public abstract class Contractor {

	private final String taxIdentifier;
	private final Address address;

	// all args constructor and getters omitted for brevity

}

There are two implementations of Contractor. One for company:

public class CompanyContractor extends Contractor {

	private final String companyName;

	// all args constructor and getters omitted for brevity

}

and one for private person:

public class PrivateContractor extends Contractor {

	private final String name;
	private final String surname;

	// all args constructor and getters omitted for brevity

}

Lastly there is an InvoiceItem class, which depicts the actual row of invoice with information about product name, quantity, price etc.:

public class InvoiceItem {

	private final String name;
	private final Unit unit;
	private final BigDecimal quantity;
	private final BigDecimal unitaryNetValue;
	private final BigDecimal vatRate;

	// all args constructor omitted for brevity

	public BigDecimal getTotalNetValue() {
		return unitaryNetValue.multiply(quantity);
	}

	public BigDecimal getTotalGrossValue() {
		return getTotalNetValue().add(getTotalVatValue());
	}

	public BigDecimal getTotalVatValue() {
		return unitaryNetValue.multiply(vatRate).multiply(quantity)
				.setScale(2, RoundingMode.HALF_EVEN);
	}

	// getters omitted for brevity

}

Currency and Unit are just custom enums.

AssertJ

AssertJ is a library that provides fluent assertions interface for Java. It comprises many interesting features like support for Java 8 lambdas, soft assertions or extensive set of assertions for working with collections in a stream like fashion. To get to know all of them I highly recommend to visit its site.

Now let’s take a look how we can use AssertJ’s default API to implement invoice generator test. The easiest to check are simple members of Invoice class:

assertThat(invoice.getCurrency()).isEqualTo(Currency.GBP);
assertThat(invoice.getDocumentInfo().getNumber()).isEqualTo(INVOICE_NUMBER);

Investigating buyer and supplier objects is much trickier. Because of the fact that Contractor has two different implementations we need to make additional checks beforehand:

assertThat(invoice.getSupplier()).isInstanceOf(CompanyContractor.class);
		
CompanyContractor companyContractor = (CompanyContractor) invoice.getSupplier();
		
assertThat(companyContractor.getCompanyName()).isEqualTo("Bananex Ltd");
assertThat(companyContractor.getAddress().getCity()).isEqualTo("Bananapolis");
// and so on

It is quite easy with AssertJ as you see. But still, we need to do instanceof check every time. It would be nice if we could forget about it and about all of the extractions which we need to do in order to ensure that a contractor has the right city value in its address property.

And there are still invoice items to check. If we would only like to make sure that couple of specific products are in the invoice then we can easily make it like this:

assertThat(invoice.getItems())
		.extracting("name")
		.containsExactlyInAnyOrder("Bananas through the ages", "Banana consulting");

But if we would like to delve deeper into InvoiceItem objects then we would need to get them from list, assuming the order in which they appear in it:

InvoiceItem bananaConsultingItem = invoice.getItems().get(1);
assertThat(bananaConsultingItem.getUnitaryNetValue()).isEqualTo(new BigDecimal("10.00"));

And please do not get me wrong — all of the above are perfectly valid and fine code examples. But if the object which we would like to check in our tests constitutes a big part of our domain, then there is a chance that we need to implement things like these quite often. In such a situation introducing our custom assertions into the code base can really pay off in the future.

Custom assertions

The creators of AssertJ have actually thought about writing custom assertions and introduced AbstractAssert class to make it possible for us. The class is generic with first type being self reference and the second one being the type of object we would like to assert. But it is best to explain by an example. So let us start with a simple one regarding the invoice object:

public class InvoiceAssertion extends AbstractAssert<InvoiceAssertion, Invoice> {

	InvoiceAssertion(Invoice invoice) {
		super(invoice, InvoiceAssertion.class);
	}

	public static InvoiceAssertion assertThat(Invoice invoice) {
		return new InvoiceAssertion(invoice);
	}

	public InvoiceAssertion hasNumber(String invoiceNumber) {
		isNotNull();
		assertDocumentInfoNotNull();
		Assertions.assertThat(actual.getDocumentInfo().getNumber())
				.describedAs("Invoice number check")
				.isEqualTo(invoiceNumber);
		return this;
	}
	
	public InvoiceAssertion isIssuedIn(Currency currency) {
		isNotNull();
		Assertions.assertThat(actual.getCurrency())
				.describedAs("Invoice currency check")
				.isEqualTo(currency);
		return this;
	}

	// other useful assertion methods

	private void assertDocumentInfoNotNull() {
		Assertions.assertThat(actual.getDocumentInfo())
				.describedAs("Invoice documentInfo check")
				.isNotNull();
	}

}

And that’s it — really simple! Our first assertion does not do much for now, but it is already very nice to use, especially with proper static import:

assertThat(invoice)
	.hasNumber(INVOICE_NO)
	.isIssuedIn(Currency.GBP);

This little snippet reads just like natural language. It is the very beginning of a domain specific language for our tests. And what is really fantastic about it is that inside our custom assertion class we still use very handy AssertJ’s default ones. So instead of building something completely new we have a possibility to build on top of the features already available.

But we came here with a problem about making assertions regarding Contractor class inside the Invoice. What can be done about it? Well let’s start with introducing custom assertion for an abstract contractor class and solving the problem with reaching deep into the object graph for checking the address property:

public class ContractorAssertion<T extends Contractor>
		extends AbstractAssert<ContractorAssertion<T>, T> {

	ContractorAssertion(T t, Class<?> selfType) {
		super(t, selfType);
	}

	public ContractorAssertion<T> hasTaxIdentifier(String taxIdentifier) {
		isNotNull();
		Assertions.assertThat(actual.getTaxIdentifier())
				.describedAs("Tax identifier number check")
				.isEqualTo(taxIdentifier);
		return this;
	}

	public ContractorAssertion<T> hasAddress(String street, String city,
											 String postalCode, String country) {
		isNotNull();
		Assertions.assertThat(actual.getAddress())
				.describedAs("Address check")
				.isEqualToComparingFieldByField(new Address(
						street,
						city,
						postalCode,
						country
				));
		return this;
	}

}

This is just a base class from which two specific implementations will inherit. One of them regards private contractor:

public class PrivateContractorAssertion extends ContractorAssertion<PrivateContractor> {

	PrivateContractorAssertion(PrivateContractor privateContractor) {
		super(privateContractor, PrivateContractorAssertion.class);
	}

	public static PrivateContractorAssertion assertThat(PrivateContractor privateContractor) {
		return new PrivateContractorAssertion(privateContractor);
	}

	public PrivateContractorAssertion hasName(String name) {
		isNotNull();
		Assertions.assertThat(actual.getName())
				.describedAs("Name check")
				.isEqualTo(name);
		return this;
	}

	// surname assertion method

}

The second one (for company) is very similar so I will omit it here.

Unfortunately none of these allow us to forget about contractor instanceof check. We just have two assertion classes now — PrivateContractorAssertion  and CompanyContractorAssertion. To overcome this issue we can introduce one nifty little class letting us forget about it:

public class ContractorAssertions extends AbstractAssert<ContractorAssertions, Contractor> {
	
	private ContractorAssertions(Contractor contractor) {
		super(contractor, ContractorAssertions.class);
	}

	public static ContractorAssertions assertThat(Contractor contractor) {
		return new ContractorAssertions(contractor);
	}
	
	public PrivateContractorAssertion isPrivateContractor() {
		isNotNull();
		Assertions.assertThat(actual)
				.describedAs("Contractor type check")
				.isInstanceOf(PrivateContractor.class);
		return PrivateContractorAssertion.assertThat((PrivateContractor) actual);
	}
	
	public CompanyContractorAssertion isCompanyContractor() {
		// ...
	}
	
}

With this little improvement now we can write assertions like the one below:

assertThat(invoice.getBuyer())
	.isPrivateContractor()
	.hasName("Tom")
	.hasSurname("Riddle")
	.hasAddress(
		"Vauxhall Street, Wool's Orphanage",
		"London",
		"SE11 5RW",
		"United Kingdom"
	);

Now the assertion part of our test will be much more readable and concise:

// invoice simple properties check
assertThat(invoice)
	.hasNumber(INVOICE_NUMBER)
	// other asserts
	.isIssuedIn(Currency.GBP);

// invoice contractor check
assertThat(invoice.getBuyer())
	.isPrivateContractor()
	// other asserts
	.hasSurname("Riddle");
		
// invoice item check
assertThat(invoice.getItems().get(0))
	.hasName("Bananas")
	// other asserts
	.hasUnitaryNetValueOf(new BigDecimal("10.00"));
We can implement assertion class for InvoiceItem in the same manner as in the above examples. It does not differ much so it is not presented here.

Nested assertions

We already have couple of nice assertions at hand, but there still remains a problem of retrieving invoice items from list by raw index. It would also be nice if we could have chained all the previous assertions in one call. And this is the time where something I call nested assertions comes in.

The idea for dealing with invoice items checks is pretty simple. If we think about our particular domain it seems quite logical to assume that every InvoiceItem will have unique name in the scope of an Invoice. So instead of getting items by an index we can add method to InvoiceAssertion, which will check if item of particular name is present in the document. If the item is really there then we would return InvoiceItemAssertion from the call. The code looks as follows:

public class InvoiceAssertion extends AbstractAssert<InvoiceAssertion, Invoice> {

	// rest of the code unchanged 

	public InvoiceItemAssertion containsItem(String name) {
		isNotNull();
		Optional<InvoiceItem> matchingItem = actual.getItems().stream()
				.filter(item -> name.equals(item.getName()))
				.findFirst();
		if (!matchingItem.isPresent()) {
			// AssertJ method
			failWithMessage("Invoice doesn't contain item with name %s", name);
		}
		return InvoiceItemAssertion.assertThat(matchingItem.get());
	}

}

and allows us to write assertion like the one below:

assertThat(invoice)
	.hasNumber(INVOICE_NUMBER)
	.hasIssuanceDateOf(ISSUANCE_DATE)
	.containsItem("Bananas through the ages")
		.hasQuantity(1)
		.hasUnit(Unit.PIECE)
		.hasUnitaryNetValueOf(new BigDecimal("29.99"));
		// cannot come back to asserting invoice

// need to start all over from the invoice to check another item
assertThat(invoice)
	.containsItem("Banana consulting")
		.hasQuantity(1);

But let’s face it – it does not look very well. Firstly, it does not allow us to chain calls further and check another item in one call. Secondly, the names of the methods do not fit in the flow of reading – there is not much of a fluency here.

Fortunately there is a trick, which will allow us to overcome these problems. We can introduce new assertion class as a wrapper for InvoiceItemAssertion. It will redefine names of the default assertion and provide a method to go up a level and continue chaining our calls. And that is this little hero:

public class NestedInvoiceItemAssertion 
		extends AbstractAssert<NestedInvoiceItemAssertion, InvoiceItem> {

	private final InvoiceAssertion parent;

	NestedInvoiceItemAssertion(InvoiceItem invoiceItem, InvoiceAssertion parent) {
		super(invoiceItem, NestedInvoiceItemAssertion.class);
		this.parent = parent;
	}

	public NestedInvoiceItemAssertion withUnit(Unit unit) {
		InvoiceItemAssertion.assertThat(actual).hasUnit(unit);
		return this;
	}

	public NestedInvoiceItemAssertion withQuantity(BigDecimal quantity) {
		InvoiceItemAssertion.assertThat(actual).hasQuantity(quantity);
		return this;
	}

	// and so on

	public InvoiceAssertion and() {
		return parent;
	}

}

The whole magic here happens because of the fact that NestedInvoiceItemAssertion contains a reference to the parent assertion (InvoiceAssertion). It allows us to use the and method to get back to the parent assertion and fluently invoke other methods on it (e.g. assert on another nested invoice item). The constructor of NestedInvoiceItemAssertion  is deliberately made package private so only assertions sitting in the same package could use it. Now there is only one small change to get it going — changing the return type of containsItem method inside InvoiceAssertion code.

If we follow like this and introduce similar utility classes also for previous assertions our final test check could look quite impressive:

assertThat(invoice)
	.hasNumber(INVOICE_NUMBER)
	.hasIssuanceDateOf(ISSUANCE_DATE)
	.hasPaymentDueDateOf(DUE_DATE)
	.isIssuedByCompany()
		.withName("Bananex Ltd")
		.withTaxIdentifier("2897943980")
	.and()
	.isIssuedForPrivatePerson()
		.withName("Tom")
		.withSurname("Riddle")
		.withTaxIdentifier("2354012266")
	.and()
	.containsItem("Bananas through the ages")
		.withQuantity(1)
		.withUnit(Unit.PIECE)
		.withUnitaryNetValueOf(new BigDecimal("29.99"))
	.and()
	.isIssuedIn(Currency.GBP);

Everyone will see instantly what is our intent here cause the code reads just like a book. And what is more, we now have pretty useful tool to reuse in other tests we will write in the future.

In some cases having many assertions in one test is an antipattern. Tests should be focused and follow the Single Responsibility principle. On the other hand, some test cases require complex assertions and it does not mean that the test checks too much. These considerations are generally outside the scope of this article. I’m just showing technical aspects of writing fluent assertion classes which can be reused in many different tests.

Introducing new classes for nested assertions could be debatable. As usual it depends on the actual situation. Let’s take InvoiceItemAssertion  as an example. If we are sure that we would not need it as a standalone assertion in other tests then there is shorter solution. We can just change method names of InvoiceItemAssertion and encompass it with parent field member as in the NestedInvoiceItemAssertion. That would reduce the amount of classes we have to implement.

If, on the other hand, it seems probable that we will need standalone invoice item assertion in the future, then introducing special class just for chained calls seems justified. The example in this article used exact type for parent reference, but it is possible to make the assertion generic on this member. So no matter how deep our nesting will go we would only need one class.

Conclusion

In this post I presented some patterns which allow one to write fluent assertions easily. The described approach allows to write the assertion logic once and then reuse it in many different test scenarios. The resulting assertion code reads almost like a book and makes tests really easy to write.

The obvious drawback of the techniques used in this article is that they require quite a lot of additional code. Because of that it can be an overkill to introduce such assertions for objects which are not likely to need a lot of tests — in such cases pure AssertJ will suffice. But the heart of the system — core domain logic — usually requires many tests (hundreds or more). Writing custom assertions pays off very quickly in such cases — we invest some time at the beginning, but then we can rapidly write many different tests. As always — everything depends on the project specifics and the context.

I hope you will find this article useful. I encourage you to revisit your core domain object test cases — perhaps there is a good spot for writing some custom assertion code?

Related Post

2 response to "Custom fluent assertions with AssertJ"

  1. Avatar

    By: Artur Wierzbicki Posted: January 21, 2018

    Cześć!

    Też lubię pisać testy w podobnym stylu (w szczególności te dotyczące wielu klas, “modułowe”), z tą różnicą że nie korzystam z żadnego frameworka (tutaj AssertJ) pod spodem.

    Mała sugestia: co byś powiedział na

    .isIssuedBy(
        company()
            .withName("Bananex Ltd")
            .withTaxIdentifier("2897943980").build()
    )
    .isIssuedFor(
        privatePerson()
            .withName("Tom")
            .withSurname("Riddle")
            .withTaxIdentifier("235").build()
    )
    .contains(
        item("Bananas through the ages")
            .withQuantity(1)
            .withUnit(Unit.PIECE)
            .withUnitaryNetValueOf(new BigDecimal("29.99")).build()
    )
    

    zamiast wszystkiego co się wiążę z “and()”? 🙂 Można by też usunąć fluent buildera/metodę .build() w prostszych przypadkach.

    • Mateusz Fedkowicz

      By: Mateusz Fedkowicz Posted: January 23, 2018

      Cześć!

      Dzięki za zainteresowanie artykułem 🙂

      A przechodząc do Twojego pytania – rozumiem, że u Ciebie w przykładzie działa to w ten sposób, że metody asercji (np. isIssuedBy(...)) przyjmują już zbudowane obiekty? Jeśli tak to z pewnością zaletą jest to, że oprócz samej asercji implementujemy tez reużywalne buildery. Natomiast widzę też kilka niedogodności w takim podejściu:

      • Wydaje mi się, że trudno będzie napisać asercję w taki sposób, aby można było pominąć sprawdzanie któregoś z pól (np. chcielibyśmy tylko sprawdzić, czy wystawiający fakturę jest firmą i ma określony identyfikator podatkowy). Możemy wprawdzie polegać na metodzie equals w budowanym obiekcie, ale tą metodę też musimy zaimplementować.
      • Jeśli zdecydujemy się tak jak powyżej opisałem zaimplementować metodę equals to nie do odróżnienia jest sytuacja, w której chcielibyśmy koniecznie sprawdzić, że jakieś pole nie ma ustawionej wartości. Jeśli podamy w builderze null dla jakiejś wartości to w samej asercji nie mamy kontekstu, aby stwierdzić czy chcieliśmy rzeczywiście potwierdzić brak wartości, czy też po prostu nie interesuje nas to pole. W moim podejściu można by dopisać krótką metodę do interesującej nas asercji — przykładowo withoutSurname()

      Podsumowując — jak zwykle wszystko zależy od konkretnego przypadku użycia. Twoje podejście z pewnością sprawdzi się w sytuacjach, kiedy rzeczywiście chcemy sprawdzać wszystkie/większość pól jakichś obiektów i zawsze tak robimy. Zagnieżdżone asercje są bardzo fajne gdy poza kilkoma wyjątkami nie chcemy badać wszystkich właściwości naszych obiektów domenowych.

      PS Przedstawionego przez Ciebie podejścia używam dość często do implementacji builderów dla bardziej skompikowanych obiektów 😉

Leave a Reply

Your email address will not be published. Required fields are marked *