TestDataCaptureJ: Explanation


Table of Contents

How does it work?
Code Examples
Simple fields
Field types
Complex fields
Collection fields
Array Fields
Map Fields
Some general observations, and exceptions
Next:

How does it work?

One of the most common uses of AspectJ is to enable tracing or logging in program code in an AOP way, that is, the logging code is separate from the program code. Also where you want to do the tracing can be changed just by the pointcut that you use to intercept the code and to insert the logging. If you are unfamiliar with AspectJ, please read the AspectJ documentation or the excellent book [AspectJ in Action].

Basically TestDataCaptureJ tool is just a fancy version of an AspectJ tracing program (and in fact grew out of a tracing tool). We define a pointcut which will intercept the program code at a point where the object that contains the test data we require is available - this means the object is a parameter or return value of some method in the program code. Having intercepted that data object we then log its contents to a file.

Where TestDataCaptureJ differs from some other tracing programs is this:

  • Instead of just logging the data values, the fields of the data object are logged in a format that can be used as java code.

  • It uses recursion to log the whole graph of objects starting with the data object that is intercepted, i.e. if the data object contains fields that are complex objects, collections, arrays or maps, then they are also recursively logged.

  • The logging doesn't happen immediately at the point of interception. Instead the recursive process uses reflection to store the metadata required in an intermediate object, then the logging is done from the information stored in the intermediate object.

  • Uses load-time weaving to insert the logging code.

Code Examples

To demonstrate some of the features of the TestDataCaptureJ tool, here are some examples of code and the logging that would be produced from them. The code and logging for the examples for the explanation page are found in:

  • 'test/au/com/dw/testdatacapturej/explanation/ExplanationTest.java'

  • 'test/au/com/dw/testdatacapturej/mock/explanation/*.java'

Run the unit test cases in 'test/au/com/dw/testdatacapturej/explanation/ExplanationTest.java' and the example logging will be output to the console. As an example the following unit test was used to generated the logging example used in the 'Array fields' section:

.
.
.
	@Test
	public void testCustomerAddressesInArray()
	{
		CustomerAddressesInArray customerAddresses = new CustomerAddressesInArray();
		
		Object[] addresses = new Object[2];
		addresses[0] = createAddress1();
		addresses[1] = createAddress2();
		customerAddresses.setAddresses(addresses);
		
		joinPointParamForCustomerAddressesInArray(customerAddresses);
	}

	
	private void joinPointParamForCustomerAddressesInArray(CustomerAddressesInArray customerAddresses)
	{
		
	}
.
.
.

Notice empty method 'joinPointParamForCustomerAddressesInArray(..)', this is the method that has a JointPoint configured to capture the contents of the parameter 'customerAddresses'.

Simple fields

The first example class just contains simple fields.

'test/au/com/dw/testdatacapturej/mock/explanation/Customer.java':

package au.com.dw.testdatacapturej.mock.explanation;

public class Customer {
	private String firstName;
	private String surName;
	
	public Customer() {
	
	}
	
	public String getFirstName() {
		return firstName;
	}
	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}
	public String getSurName() {
		return surName;
	}
	public void setSurName(String surName) {
		this.surName = surName;
	}
	
	}

The TestDataCaptureJ tool should generate the following log if the Customer object containing some data was intercepted.

// au.com.dw.testdatacapturej.explanation.ExplanationTest.joinPointParamForCustomer:Parameter1
public au.com.dw.testdatacapturej.mock.explanation.Customer createParam1Customer_au_com_dw_testdatacapturej_explanation_ExplanationTest_joinPointParamForCustomer() {

au.com.dw.testdatacapturej.mock.explanation.Customer customer0 = new au.com.dw.testdatacapturej.mock.explanation.Customer();
customer0.setFirstName("John");
customer0.setSurName("Smith");

return customer0;
}

We can examine the logging to see what the TestDataCaptureJ tool has generated for this class (I've just manually added some line numbers to the logging):

1. // au.com.dw.testdatacapturej.explanation.ExplanationTest.joinPointParamForCustomer:Parameter1
2. public au.com.dw.testdatacapturej.mock.explanation.Customer createParam1Customer_au_com_dw_testdatacapturej_explanation_ExplanationTest_joinPointParamForCustomer() {

3. au.com.dw.testdatacapturej.mock.explanation.Customer customer0 = new au.com.dw.testdatacapturej.mock.explanation.Customer();
4. customer0.setFirstName("John");
5. customer0.setSurName("Smith");

6. return customer0;
7. }

  • It has generated a comment line that just shows the name of the method that was intercepted (line 1).

  • It has generated a java method to contains the logging of the fields in the Customer object (lines 2, 6 and 7). The name of the method is created based on the intercepted method name, the type of the data object and the parameter position (or just a return value).

  • It has generated a java line to invoke the constructor on the data object to create an instance of the object (line 3). Here we can see why the TestDataCaptureJ tool requires that the data object and its fields should have default no-argument constructors.

  • After capturing the value of the fields in the data object, it has generated a java line to invoke a setter method for each field (lines 4 and 5). Here we can see why the TestDataCaptureJ tool requires the the fields of the data object has setter methods that follow the standard naming convention.

Field types

This example show how the values of the different built-in types are handled.

'test/au/com/dw/testdatacapturej/mock/explanation/Account.java':

package au.com.dw.testdatacapturej.mock.explanation;

public class Account {
	private String name;
	private Character type;
	private Integer number;
	private long customerNumber;
	private boolean isActive;
	private double amount;
	private Float interestRate;
	
	public Account() {
		super();
	}
	
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public Character getType() {
		return type;
	}
	public void setType(Character type) {
		this.type = type;
	}
	public Integer getNumber() {
		return number;
	}
	public void setNumber(Integer number) {
		this.number = number;
	}
	public long getCustomerNumber() {
		return customerNumber;
	}
	public void setCustomerNumber(long customerNumber) {
		this.customerNumber = customerNumber;
	}
	public boolean isActive() {
		return isActive;
	}
	public void setActive(boolean isActive) {
		this.isActive = isActive;
	}
	public double getAmount() {
		return amount;
	}
	public void setAmount(double amount) {
		this.amount = amount;
	}
	public Float getInterestRate() {
		return interestRate;
	}
	public void setInterestRate(Float interestRate) {
		this.interestRate = interestRate;
	}
	
}

The TestDataCaptureJ tool should generate the following log if the Account object containing some data was intercepted.

public au.com.dw.testdatacapturej.mock.explanation.Account createReturnAccount_au_com_dw_testdatacapturej_explanation_ExplanationTest_joinPointReturnForAccount() {

au.com.dw.testdatacapturej.mock.explanation.Account account0 = new au.com.dw.testdatacapturej.mock.explanation.Account();
account0.setName("Savings Account");
account0.setType('A');
account0.setNumber(123);
account0.setCustomerNumber(1234567890L);
account0.setIsActive(true);
account0.setAmount(1000.5d);
account0.setInterestRate(0.1f);

return account0;
}

Notice that in the generated setter methods the values are formatted in a way that is appropriate for its type:

  • Strings are surrounded by double quotes ["], so the value [Savings Account] becomes ["Savings Account"].

  • Characters are surround by single quotes ['], so the value [A] becomes ['A'].

  • Longs are suffixed by [L], so the value [1234567890] becomes [1234567890L].

  • Doubles are suffixed by [d], so the value [1000.5] becomes [1000.5d].

  • Floats are suffixed by [f], so the value [0.1] becomes [0.1f].

Note that is should not matter if the fields were primitive types or their wrapper classes, i.e. Integer or int, Character or char, etc.

Complex fields

This is an example of a class where the field is another complex class, i.e. not just a primitive value. Here the CustomerAddress class contains a field which is of the Address class.

'test/au/com/dw/testdatacapturej/mock/explanation/Address.java':

package au.com.dw.testdatacapturej.mock.explanation;

public class Address {
	private int houseNumber;
	private String street;
	private String city;
	
	public Address() {

	}

	public int getHouseNumber() {
		return houseNumber;
	}

	public void setHouseNumber(int houseNumber) {
		this.houseNumber = houseNumber;
	}

	public String getStreet() {
		return street;
	}

	public void setStreet(String street) {
		this.street = street;
	}

	public String getCity() {
		return city;
	}

	public void setCity(String city) {
		this.city = city;
	}
		
}

'test/au/com/dw/testdatacapturej/mock/explanation/CustomerAddress.java':

package au.com.dw.testdatacapturej.mock.explanation;

public class CustomerAddress {
	private Address address;

	public CustomerAddress() {
		
	}

	public Address getAddress() {
		return address;
	}

	public void setAddress(Address address) {
		this.address = address;
	}
	
}

The TestDataCaptureJ tool should generate the following log if the CustomerAddress object containing some data was intercepted.

public au.com.dw.testdatacapturej.mock.explanation.CustomerAddress createReturnCustomerAddress_au_com_dw_testdatacapturej_explanation_ExplanationTest_joinPointReturnForCustomerAddress() {

au.com.dw.testdatacapturej.mock.explanation.CustomerAddress customerAddress0 = new au.com.dw.testdatacapturej.mock.explanation.CustomerAddress();

au.com.dw.testdatacapturej.mock.explanation.Address address1 = new au.com.dw.testdatacapturej.mock.explanation.Address();
address1.setHouseNumber(1);
address1.setStreet("Home Street");
address1.setCity("Sydney");
customerAddress0.setAddress(address1);

return customerAddress0;
}

We can examine the logging to see what the TestDataCaptureJ tool has generated for this class (I've just manually added some line numbers to the logging):

1. public au.com.dw.testdatacapturej.mock.explanation.CustomerAddress createReturnCustomerAddress_au_com_dw_testdatacapturej_explanation_ExplanationTest_joinPointReturnForCustomerAddress() {

2. au.com.dw.testdatacapturej.mock.explanation.CustomerAddress customerAddress0 = new au.com.dw.testdatacapturej.mock.explanation.CustomerAddress();

3. au.com.dw.testdatacapturej.mock.explanation.Address address1 = new au.com.dw.testdatacapturej.mock.explanation.Address();
4. address1.setHouseNumber(1);
5. address1.setStreet("Home Street");
6. address1.setCity("Sydney");
7. customerAddress0.setAddress(address1);

8. return customerAddress0;
9. }

  • It has generated a java method to contains the logging of the fields in the CustomerAddress object (lines 1, 8 and 9).

  • It has generated a java line to invoke the constructor on the data object that was intercepted (CustomerAddress) to create an instance of the object (line 2).

  • It has generated a java line to invoke the constructor on the field object (Address) to create an instance of the object (line 3).

  • It has generated a java line to invoke a setter method for each field in the field object (Address) (lines 4, 5 and 6).

  • It has generated a java line to invoke a setter method for each field in the data object (CustomerAddress) (line 7). This assigns the field object (Address) to the data object (CustomerAddress).

  • Notice that the field names generated are suffixed by a number (customerAddress0 in line 2 and address1 in line 3). In order to prevent duplicate field names in the generated code, there is a counter that adds an incremented number to the field name to each type of complex field - Object, Collection, Array or Map. For instance if the CustomerAddress class has 3 Address fields then the generated field names would be: customerAddress0, address1, address2, address3.

Collection fields

This is an example of a class where the field is a Collection. The collection can contain simple values or complex objects.

'test/au/com/dw/testdatacapturej/mock/explanation/CustomerAddressesInCollection.java':

package au.com.dw.testdatacapturej.mock.explanation;

import java.util.Collection;

public class CustomerAddressesInCollection {
	private Collection<?> addresses;

	public CustomerAddressesInCollection() {
		
	}

	public Collection<?> getAddresses() {
		return addresses;
	}

	public void setAddresses(Collection<?> addresses) {
		this.addresses = addresses;
	}
	
}

The TestDataCaptureJ tool should generate the following log if the CustomerAddressesInCollection object containing some data was intercepted.

// au.com.dw.testdatacapturej.explanation.ExplanationTest.joinPointParamForCustomerAddressesInCollection:Parameter1
public au.com.dw.testdatacapturej.mock.explanation.CustomerAddressesInCollection createParam1CustomerAddressesInCollection_au_com_dw_testdatacapturej_explanation_ExplanationTest_joinPointParamForCustomerAddressesInCollection() {

au.com.dw.testdatacapturej.mock.explanation.CustomerAddressesInCollection customerAddressesInCollection0 = new au.com.dw.testdatacapturej.mock.explanation.CustomerAddressesInCollection();

java.util.ArrayList arrayList0 = new java.util.ArrayList();

au.com.dw.testdatacapturej.mock.explanation.Address address1 = new au.com.dw.testdatacapturej.mock.explanation.Address();
address1.setHouseNumber(1);
address1.setStreet("Home Street");
address1.setCity("Sydney");
arrayList0.add(address1);
arrayList0.add("Test value");

customerAddressesInCollection0.setAddresses(arrayList0);

return customerAddressesInCollection0;
}

We can examine the logging to see what the TestDataCaptureJ tool has generated for this class (I've just manually added some line numbers to the logging):

1.  // au.com.dw.testdatacapturej.explanation.ExplanationTest.joinPointParamForCustomerAddressesInCollection:Parameter1
2.  public au.com.dw.testdatacapturej.mock.explanation.CustomerAddressesInCollection createParam1CustomerAddressesInCollection_au_com_dw_testdatacapturej_explanation_ExplanationTest_joinPointParamForCustomerAddressesInCollection() {

3.  au.com.dw.testdatacapturej.mock.explanation.CustomerAddressesInCollection customerAddressesInCollection0 = new au.com.dw.testdatacapturej.mock.explanation.CustomerAddressesInCollection();

4.  java.util.ArrayList arrayList0 = new java.util.ArrayList();

5.  au.com.dw.testdatacapturej.mock.explanation.Address address1 = new au.com.dw.testdatacapturej.mock.explanation.Address();
6.  address1.setHouseNumber(1);
7.  address1.setStreet("Home Street");
8.  address1.setCity("Sydney");
9.  arrayList0.add(address1);
10. arrayList0.add("Test value");

11. customerAddressesInCollection0.setAddresses(arrayList0);

12. return customerAddressesInCollection0;
13. }

  • It has generated a comment line that just shows the name of the method that was intercepted (line 1).

  • It has generated a java method to contains the logging of the fields in the CustomerAddressesInCollection object (lines 2, 12 and 13).

  • It has generated a java line to invoke the constructor on the data object that was intercepted (CustomerAddressesInCollection) to create an instance of the object (line 3).

  • It has generated a java line to invoke the constructor on the Collection field object to create an instance of the collection (line 4).

  • It has generated a java line to invoke the constructor on a complex object that is an element of the collection (line 5).

  • It has generated a java line to invoke a setter method for each field in the complex object stored in the collection (Address) (lines 6, 7 and 8).

  • It has generated a java line to add the complex object (Address) to the collection (line 9).

  • It has generated a java line to add a simple value to the collection (line 10). I guess logically could have added another address instead, but wanted to demonstrate adding a simple value to the collection.

  • It has generated a java line to invoke a setter method for the Collection field in the data object (CustomerAddressesInCollection) (line 11). This assigns the field object (Collection) to the data object (CustomerAddressesInCollection).

  • Once again notice that the field names generated are suffixed by a number (e.g. arrayList0 in line 4). If the CustomerAddressesInCollection class has 3 Collection fields instead of 1, then the generated field names would be: arrayList0, arrayList1, arrayList2.

Array Fields

This is an example of a class where the field is an array, which can also contain either simple values or complex objects.

'test/au/com/dw/testdatacapturej/mock/explanation/CustomerAddressesInArray.java':

public class CustomerAddressesInArray {
	private Object[] addresses;

	public CustomerAddressesInArray() {
	}

	public Object[] getAddresses() {
		return addresses;
	}

	public void setAddresses(Object[] addresses) {
		this.addresses = addresses;
	}
	
}

The TestDataCaptureJ tool should generate the following log if the CustomerAddressesInArray object containing some data was intercepted.

// au.com.dw.testdatacapturej.explanation.ExplanationTest.joinPointParamForCustomerAddressesInArray:Parameter1
public au.com.dw.testdatacapturej.mock.explanation.CustomerAddressesInArray createParam1CustomerAddressesInArray_au_com_dw_testdatacapturej_explanation_ExplanationTest_joinPointParamForCustomerAddressesInArray() {

au.com.dw.testdatacapturej.mock.explanation.CustomerAddressesInArray customerAddressesInArray0 = new au.com.dw.testdatacapturej.mock.explanation.CustomerAddressesInArray();

java.lang.Object[] objectArray0 = new java.lang.Object[2];

au.com.dw.testdatacapturej.mock.explanation.Address address1 = new au.com.dw.testdatacapturej.mock.explanation.Address();
address1.setHouseNumber(1);
address1.setStreet("Home Street");
address1.setCity("Sydney");
objectArray0[0] = address1;

au.com.dw.testdatacapturej.mock.explanation.Address address2 = new au.com.dw.testdatacapturej.mock.explanation.Address();
address2.setHouseNumber(2);
address2.setStreet("Work Road");
address2.setCity("London");
objectArray0[1] = address2;

customerAddressesInArray0.setAddresses(objectArray0);

return customerAddressesInArray0;
}

We can examine the logging to see what the TestDataCaptureJ tool has generated for this class (I've just manually added some line numbers to the logging):

1.  // au.com.dw.testdatacapturej.explanation.ExplanationTest.joinPointParamForCustomerAddressesInArray:Parameter1
2.  public au.com.dw.testdatacapturej.mock.explanation.CustomerAddressesInArray createParam1CustomerAddressesInArray_au_com_dw_testdatacapturej_explanation_ExplanationTest_joinPointParamForCustomerAddressesInArray() {

3.  au.com.dw.testdatacapturej.mock.explanation.CustomerAddressesInArray customerAddressesInArray0 = new au.com.dw.testdatacapturej.mock.explanation.CustomerAddressesInArray();

4.  java.lang.Object[] objectArray0 = new java.lang.Object[2];

5.  au.com.dw.testdatacapturej.mock.explanation.Address address1 = new au.com.dw.testdatacapturej.mock.explanation.Address();
6.  address1.setHouseNumber(1);
7.  address1.setStreet("Home Street");
8.  address1.setCity("Sydney");
9.  objectArray0[0] = address1;

10. au.com.dw.testdatacapturej.mock.explanation.Address address2 = new au.com.dw.testdatacapturej.mock.explanation.Address();
11. address2.setHouseNumber(2);
12. address2.setStreet("Work Road");
13. address2.setCity("London");
14. objectArray0[1] = address2;

15. customerAddressesInArray0.setAddresses(objectArray0);

16. return customerAddressesInArray0;
17. }

  • It has generated a comment line that just shows the name of the method that was intercepted (line 1).

  • It has generated a java method to contains the logging of the fields in the CustomerAddressesInArray object (lines 2, 16 and 17).

  • It has generated a java line to invoke the constructor on the data object that was intercepted (CustomerAddressesInArray) to create an instance of the object (line 3).

  • It has generated a java line to create an instance of the array field object (line 4). Note that it is an array of the type java.lang.Object rather than the specific class, and that it should be sized correctly to hold the correct number of elements.

  • It has generated a java line to invoke the constructors on the complex objects that are the elements of the array, in this case there are 2 (line 5 and 10).

  • It has generated a java line to invoke a setter method for each field in the complex object stored in the collection (Address) (lines 6, 7 and 8 for the first element and lines 11, 12 and 13 for the second element).

  • It has generated a java line to assign the complex objects (Address) to the aray (line 9 and 14).

  • It has generated a java line to invoke a setter method for the array field in the data object (CustomerAddressesInArray) (line 15). This assigns the field object (Object[]) to the data object (CustomerAddressesInArray).

  • Once again notice that the field names generated are suffixed by a number to make them unique (e.g. address1 in line 5 and address2 in line 10).

Map Fields

This is an example of a class where the field is a Map. The Map can contain simple values or complex objects.

'test/au/com/dw/testdatacapturej/mock/explanation/CustomerAddressesInMap.java':

public class CustomerAddressesInMap {
	private HashMap<?, ?> addresses;

	public CustomerAddressesInMap() {
	}

	public HashMap<?, ?> getAddresses() {
		return addresses;
	}

	public void setAddresses(HashMap<?, ?> addresses) {
		this.addresses = addresses;
	}
	
}

The TestDataCaptureJ tool should generate the following log if the CustomerAddressesInMap object containing some data was intercepted.

// au.com.dw.testdatacapturej.explanation.ExplanationTest.joinPointParamForCustomerAddressesInMap:Parameter1
public au.com.dw.testdatacapturej.mock.explanation.CustomerAddressesInMap createParam1CustomerAddressesInMap_au_com_dw_testdatacapturej_explanation_ExplanationTest_joinPointParamForCustomerAddressesInMap() {

au.com.dw.testdatacapturej.mock.explanation.CustomerAddressesInMap customerAddressesInMap0 = new au.com.dw.testdatacapturej.mock.explanation.CustomerAddressesInMap();

java.util.HashMap hashMap0 = new java.util.HashMap();

au.com.dw.testdatacapturej.mock.explanation.Address address1 = new au.com.dw.testdatacapturej.mock.explanation.Address();
address1.setHouseNumber(2);
address1.setStreet("Work Road");
address1.setCity("London");
hashMap0.put("work", address1);

au.com.dw.testdatacapturej.mock.explanation.Address address2 = new au.com.dw.testdatacapturej.mock.explanation.Address();
address2.setHouseNumber(1);
address2.setStreet("Home Street");
address2.setCity("Sydney");
hashMap0.put("home", address2);

customerAddressesInMap0.setAddresses(hashMap0);

return customerAddressesInMap0;
}

We can examine the logging to see what the TestDataCaptureJ tool has generated for this class (I've just manually added some line numbers to the logging):

1.  // au.com.dw.testdatacapturej.explanation.ExplanationTest.joinPointParamForCustomerAddressesInMap:Parameter1
2.  public au.com.dw.testdatacapturej.mock.explanation.CustomerAddressesInMap createParam1CustomerAddressesInMap_au_com_dw_testdatacapturej_explanation_ExplanationTest_joinPointParamForCustomerAddressesInMap() {

3.  au.com.dw.testdatacapturej.mock.explanation.CustomerAddressesInMap customerAddressesInMap0 = new au.com.dw.testdatacapturej.mock.explanation.CustomerAddressesInMap();

4.  java.util.HashMap hashMap0 = new java.util.HashMap();

5.  au.com.dw.testdatacapturej.mock.explanation.Address address1 = new au.com.dw.testdatacapturej.mock.explanation.Address();
6.  address1.setHouseNumber(2);
7.  address1.setStreet("Work Road");
8.  address1.setCity("London");
9.  hashMap0.put("work", address1);

10. au.com.dw.testdatacapturej.mock.explanation.Address address2 = new au.com.dw.testdatacapturej.mock.explanation.Address();
11. address2.setHouseNumber(1);
12. address2.setStreet("Home Street");
13. address2.setCity("Sydney");
14. hashMap0.put("home", address2);

15. customerAddressesInMap0.setAddresses(hashMap0);

16. return customerAddressesInMap0;
17. }

  • It has generated a comment line that just shows the name of the method that was intercepted (line 1).

  • It has generated a java method to contains the logging of the fields in the CustomerAddressesInMap object (lines 2, 16 and 17).

  • It has generated a java line to invoke the constructor on the data object that was intercepted (CustomerAddressesInMap) to create an instance of the object (line 3).

  • It has generated a java line to invoke the constructor on the Map field object to create an implementation instance of the Map (line 4).

  • It has generated a java line to invoke the constructor on the complex objects that are the values in the entries of the Map (line 5 and 10).

  • It has generated a java line to invoke a setter method for each field in the complex objects stored in the Map (Address) (lines 6, 7 and 8 for the first and lines 11, 12 and 13 for the second).

  • It has generated a java line to put the complex objects (Address) into the Map keyed by different values (line 9 and 14).

  • It has generated a java line to invoke a setter method for the Map field in the data object (CustomerAddressesInMap) (line 15). This assigns the field object (Map) to the data object (CustomerAddressesInMap).

Some general observations, and exceptions

  • All generated class names are fully qualified, the TestDataCaptureJ tool does not current generate import statements.

  • For complex objects such as Object, Collection, Array and Map, the field names for classes of these types are all suffixed by a number to keep them unique (e.g. address1, address2, address3, etc). Internally there is an index counter for each of the classes, and that is used for the numerical suffix.

  • Currently does not handle varargs for parameters in the intercepted method properly, e.g. 'someMethod(Object... args)'.

  • Currently does not log static fields for intercepted objects.

Next:

How to customize the logging