My favorite part of my job is writing familiar code in familiar codebases. I like being in a place where I know how most of the pieces connect, recognize the patterns, and have my environment set up so that I can run code locally and debug. It’s a special feeling.
So of course I experienced anxiety this week when I had to do the opposite of that: update a process that I only partially understood, in Java, complete with a new test. It turned out to be a really enriching experience and helped me build confidence, primarily because of the people I worked with. The first lesson this week is that having a good team makes a world of difference. Everyone was patient with me as I missed obvious bugs, submitted a PR with redundant code, and asked clarifying questions about the feedback I received. One particular case that stood out was a reviewer who asked me to organize my test into arrange/act/assert format. He had left another comment on the PR thanking me for writing a detailed description, so I wanted to try my best to live up to that positive impression and make his requested change to my PR. I set about learning what arrange/act/assert meant.
The Test
Let’s add some context by looking at the test I was actually trying to write. As usual, I’ll provide a simplified version of my code:
@Test
public void anyEvent_withCoupon_returnCouponPayload() throws Exception { Subscriber subscriber = new Subscriber();
subscriber.setCoupons(Lists.newArrayList("c1", "c2"));
when(subscriberService.getSubscriber(
any(Company.class), any(Subscriber.class))
).thenReturn(subscriber); IntegrationEvent integrationEvent =
IntegrationEventBuilder.buildIntegrationEvent();
integrationEvent.getMetadata().put("coupon", "coupon");
String input = BaseEncoding.base64().encode(
integrationEvent.toProto().toByteArray()
); CompanyDto companyDto = new CompanylDto();
CompanyDto.Profile profile =
new CompanyDto.Profile();
profile.putDataField("phoneNumber", "+15555555555");
profile.putDataField("coupon", "c1");
profile.setEmail("test@diazco.com");
companylDto.setSubscribers(Lists.newArrayList(profile)); MockEndpoint mockEndpoint = getMockEndpoint();
mockEndpoint.expectedBodiesReceived(
json.toJson(companyDto)
);
template.sendBody("direct:hitme", input); assertMockEndpointsSatisfied();
}
There’s a lot happening here, which incidentally is why I was asked to reformat this. For now, let’s focus on understanding the process piece-by-piece:
@Test
public void anyEvent_withCoupon_returnCouponPayload() throws Exception {
The first line, @Test
, is a Spring annotation that indicates to the program that this code should be executed when we are running tests. It is a public
method that does not return any value (void
) and can throw an Exception
. The test’s name is broken up into three parts:
- The type of event we’re testing (any event). This test checks the functionality of an adapter that takes one network event and uses it to create a second event to send to a third-party API. In this case, the test should work the same for any type of event that we can generate.
- The specific property of the event that makes it unique (with coupon). The event can be of any type, but if it includes a
coupon
property, we want to make sure it’s going to result in a specifically formatted payload. - The type of payload (coupon). The idea here is that if the triggering event includes a
coupon
property, the resulting API call should also include that value. The original adapter didn’t include this feature, so now that I’ve added it, I want my test to confirm that it’ll work.
Subscriber subscriber = new Subscriber();
subscriber.setCoupons(Lists.newArrayList("c1", "c2"));
when(subscriberService.getSubscriber(
any(Company.class), any(Subscriber.class))
).thenReturn(subscriber);
Here’s the first import that I have provided no context for in my snippet. We can’t see exactly what a Subscriber
is, but we’re creating an instance of one and then we’re using a setter method to assign it a new list of coupons. Subscribers certainly have other properties, but they aren’t relevant for this example.
The when
and thenReturn
syntax is a Mockito testing method that substitutes a given value if certain logic is run. Normally, my adapter would use getSubscriber
to identify a user associated with the event we are testing. For the test, we want to return this sample subscriber we’ve created rather than making a real API call.
IntegrationEvent integrationEvent =
IntegrationEventBuilder.buildIntegrationEvent();
integrationEvent.getMetadata().put("coupon", "coupon");
String input = BaseEncoding.base64().encode(
integrationEvent.toProto().toByteArray()
);
The IntegrationEvent is the “any event” or “triggering event” that we’ve been discussing in vague terms. A subscriber is doing something, which results in an event. We want this event to trigger a second network request to a third party that we’ve integrated with. In this step, we create a generic event and then use the put
method to add "coupon": "coupon"
to its metadata. The metadata property is a JSON string that is deconstructed by our adapter, which checks for different values to run different parts of the code. Adding coupon
to the metadata should result in the created subscriber’s coupon value being included in the second payload.
On the last line, we take our integrationEvent and encode it with base64. It must be in this form to be read by our adapter.
CompanyDto companyDto = new CompanylDto();
CompanyDto.Profile profile =
new CompanyDto.Profile();
profile.putDataField("phoneNumber", "+15555555555");
profile.putDataField("coupon", "c1");
profile.setEmail("test@diazco.com");
companylDto.setSubscribers(Lists.newArrayList(profile));
Next, we start building our expected result. We instantiate a CompanyDTO, which stands for Data Transfer Object or…an object that transfers data. In this case, we’re transferring data to our integrated third party via the “second payload.” We expect that our adapter will independently create a payload that looks like this based on the original event we created, so now we’re doing the same in our test to make sure they match.
The DTO defines a Profile
class, which includes subscriber information like phone number, email, and coupon. We build the profile and then append it to the DTO via the setSubscribers
method.
MockEndpoint mockEndpoint = getMockEndpoint();
mockEndpoint.expectedBodiesReceived(
json.toJson(companyDto)
);
template.sendBody("direct:hitme", input);assertMockEndpointsSatisfied();
Finally, we can start checking whether our values match up. We set up a mock endpoint and then tell it that we’re expecting to see the DTO we’ve written in JSON form. Then we send our input
, which is the encoded integrationEvent we wrote. The expectation is that, after this event goes through our adapter, it’ll send off a second event that matches the DTO. On the last line, we assert this to be the case.
Arrange-Act-Assert
After a lot of trial and error, I got the test to pass. So why did my PR reviewer ask me to edit it? As I learned while writing the code and writing this blog post, tests can be hard to read! If we add comments, they’ll be easier to understand for our future selves and any other engineers who work on the codebase. The principle here is that we separate the code into three parts and annotate each to clarify their purpose:
- Arrange: Our setup steps. This encompasses building the sample subscriber, integrationEvent, and DTO.
- Act: Executing the logic we want to test. For us, I’d say it’s just one line:
template.sendBody("direct:hitme", input);
. This is the only part of the code that actually invokes the adapter logic (not seen in this post). - Assert: Our tests will always have an assertion which determines whether they’ve passed or failed. In the real test, there are several different assertions, but I reduced that down to one for this post:
assertMockEndpointSatisfied();
.
And so, with annotations, my code looked more like this:
@Test
public void anyEvent_withCoupon_returnCouponPayload() throws Exception { // arrange
Subscriber subscriber = new Subscriber();
subscriber.setCoupons(Lists.newArrayList("c1", "c2"));
when(subscriberService.getSubscriber(
any(Company.class), any(Subscriber.class))
).thenReturn(subscriber); IntegrationEvent integrationEvent =
IntegrationEventBuilder.buildIntegrationEvent();
integrationEvent.getMetadata().put("coupon", "coupon");
String input = BaseEncoding.base64().encode(
integrationEvent.toProto().toByteArray()
); CompanyDto companyDto = new CompanylDto();
CompanyDto.Profile profile =
new CompanyDto.Profile();
profile.putDataField("phoneNumber", "+15555555555");
profile.putDataField("coupon", "c1");
profile.setEmail("test@diazco.com");
companylDto.setSubscribers(Lists.newArrayList(profile)); // act
MockEndpoint mockEndpoint = getMockEndpoint();
mockEndpoint.expectedBodiesReceived(
json.toJson(companyDto)
);
template.sendBody("direct:hitme", input); // assert
assertMockEndpointsSatisfied();
}
I ended up making act
three lines instead of one since I felt like expectedBodiesReceived
belonged in there rather than the arrange step.
Finding the answers
It was exciting to search for “arrange/act/assert” and find an immediately useful explanation in the very first result. I appreciated being able to go back to my reviewer, clarify that he was looking for comments, and then finally get the approval after making changes. In the past, I would have been too intimidated to directly ask for clarification from a real engineer since we’re often conditioned to think of ourselves as imposters. Now, I’m coming to realize that there are things we can do to break down that barrier, like what my reviewer did. Share kind words, be open to conversation, and be patient when it comes to questions. Creating a collaborative environment helps everyone.
Sources:
- Mockito thenReturn vs thenAnswer, Stacktraceguru
- Data Transfer Object DTO Definition and Usage, Okta
- Arrange-Act-Assert: A Pattern for Writing Good Tests, Automation Panda