15/03/18
Test data generation
Aspect Oriented Programming using Spring
Consider an invoice object:
Constructing this object in application code may be done in one place, but in tests it becomes messy and hard to read when this object is created multiple times
Invoice invoice = new Invoice(
new Recipient("Sherlock Holmes",
new Address("221b Baker Street",
"London",
new PostCode("NW1", "3RX"))),
new InvoiceLines(
new InvoiceLine("Deerstalker Hat",
new PoundsShillingsPence(0, 3, 10)),
new InvoiceLine("Tweed Cape",
new PoundsShillingsPence(0, 4, 12))));
It can be tempting to create an object mother, TestInvoices
Invoice invoice = TestInvoices.newDeerstalkerAndCapeInvoice();
However this does not cope well with variations in test data
Invoice invoice1 = TestInvoices.newDeerstalkerAndCapeAndSwordstickInvoice();
Invoice invoice2 = TestInvoices.newDeerstalkerAndBootsInvoice();
...
A solution is to use the builder pattern
public class InvoiceBuilder {
Recipient recipient = new RecipientBuilder().build();
InvoiceLines lines = new InvoiceLines(new InvoiceLineBuilder().build());
PoundsShillingsPence discount = PoundsShillingsPence.ZERO;
public InvoiceBuilder withRecipient(Recipient recipient) {
this.recipient = recipient;
return this;
}
public InvoiceBuilder withInvoiceLines(InvoiceLines lines) {
this.lines = lines;
return this;
}
public InvoiceBuilder withDiscount(PoundsShillingsPence discount) {
this.discount = discount;
return this;
}
public Invoice build() {
return new Invoice(recipient, lines, discount);
}
}
Tests that don't care about the precise values in an invoice can create one in a single line
Invoice anInvoice = new InvoiceBuilder().build();
Tests that want to use specific values can define them inline without filling the test with unimportant details
Invoice invoiceWithNoPostcode = new InvoiceBuilder()
.withRecipient(new RecipientBuilder()
.withAddress(new AddressBuilder()
.withNoPostcode()
.build())
.build())
.build();
Rather than calling build on each object, you can just pass the builder through
Invoice invoice = new InvoiceBuilder()
.withRecipient(new RecipientBuilder()
.withAddress(new AddressBuilder()
.withNoPostcode())))
.build();
Invoice invoiceWithNoPostcode = new InvoiceBuilder()
.withRecipient(new RecipientBuilder()
.withAddress(new AddressBuilder()
.withNoPostcode()
.build())
.build())
.build();
vs
We can de-emphasise the builders further by instantiating them in clearly named factory methods:
Order order =
anOrder().fromCustomer(
aCustomer().withAddress(
anAddress().withNoPostcode())).build();
This can be taken even further by renaming method names to reflect the relationship between objects only
Order order =
anOrder().from(aCustomer().with(anAddress().withNoPostcode())).build();
This relies on method overloading, but should only be done on objects with unique user defined types.
Primitive fields should have longer more descriptive names.
Address aLongerAddress = anAddress()
.withStreet("222b Baker Street")
.withCity("London")
.with(postCode("NW1", "3RX"))
.build();
you can initialise a single builder with the common state and then repeatedly call its build method
InvoiceBuilder products = new InvoiceBuilder()
.withLine("Deerstalker Hat", new PoundsShillingsPence(0, 3, 10))
.withLine("Tweed Cape", new PoundsShillingsPence(0, 4, 12));
Invoice invoiceWithDiscount = products
.withDiscount(0.10)
.build();
Invoice invoiceWithGiftVoucher = products
.withGiftVoucher("12345")
.build();
However you must be careful because objects built later will be created with the same state as those created earlier unless it is explicitly overridden
A solution is to add a method or copy constructor to the builder that copies state from another builder:
InvoiceBuilder products = new InvoiceBuilder()
.withLine("Deerstalker Hat", new PoundsShillingsPence(0, 3, 10))
.withLine("Tweed Cape", new PoundsShillingsPence(0, 4, 12));
Invoice invoiceWithDiscount = new InvoiceBuilder(products)
.withDiscount(0.10)
.build();
Invoice invoiceWithGiftVoucher = new InvoiceBuilder(products)
.withGiftVoucher("12345")
.build();
Separating these cross-cutting concerns from the business logic is where aspect oriented programming (AOP ) typically goes to work.
Weaving is the process of applying aspects to a target object. The weaving can take place at several points in the target object’s lifetime
Compile time - Aspects are woven in when the target class is compiled. This requires a special compiler.
Class load time - Aspects are woven in when the target class is loaded into the JVM. This requires a special ClassLoader.
Runtime - Aspects are woven in during the execution of the application. Typically, the AOP container generates a proxy object that delegates to the target object while weaving in the aspects.
Advice
The job of an aspect is called advice. Advice defines both the what and the when of an aspect.
- @AfterReturning
- @AfterThrowing
- @After
- @Around
- @Before
Spring aspects can work with five kinds of advice:
Join Points
These are the points where your aspect’s code can be inserted into the normal flow of your application to add new behaviour. e.g. method calls, fields modified, exceptions thrown.
Pointcuts
If advice defines the what and when of aspects, then pointcuts define the where. A pointcut definition matches one or more join points at which advice should be woven.
execution - matching the <Person findById(UUID uuid)> method
@Pointcut("execution(Person com.example.PersonRepository.findById(UUID))")
@Pointcut("execution(* com.example.PersonRepository.*(..))")
execution - matching all methods PersonRepository which take at least a uuid param
@Pointcut("execution(* com.example.PersonRepository.*(UUID,..))")
execution - matching all methods PersonRepository
within - matching all methods PersonRepository
@Pointcut("within(com.example.*)")
within - matching all methods in package com.example.*
@Pointcut("within(com.example.PersonRepository)")
this - matching methods where the bean reference is an instance of the given type
@Pointcut("this(com.example.Foo)")
target - matching methods where target object is an instance of the given type
@Pointcut("target(com.example.Bar)")
@Pointcut("@args(com.example.annotations.Entity)")
@args - matching methods that accept beans annotated with @Entity
@Pointcut("@annotation(com.example.Loggable)")
@within - matching methods of a class annotated with @Repository
@Pointcut("@target(org.springframework.stereotype.Repository)")
@target - matching methods of a class annotated with @Repository
@Pointcut("@within(org.springframework.stereotype.Repository)")
@annotation - matching methods annotated with @Loggable
// only methods in class annotated with @Repository
@Pointcut("@target(org.springframework.stereotype.Repository)")
public void repositoryMethods() {}
// only methods that are named findX with at least a uuid param
@Pointcut("execution(* *..find*(UUID,..))")
public void findMethods() {}
// within the package com.example
@Pointcut("within(com.example.*")
public void inExample() {}
// within the package com.example.excluded
@Pointcut("within(com.example.excluded")
public void inExcluded() {}
// combined
@Pointcut("repositoryMethods() && findMethods() && inExample() && !inExcluded()")
public void repositoryFindMethodsInExampleButNotInExcluded() {}
Pointcut expressions can be combined using
&&, || and ! operators