What I Learned at Work this Week: Arrange-Act-Assert

Photo by iOnix from Pexels

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:

--

--

--

Solutions Engineer

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

NULS has Implemented Cross-Chain Asset Circulation with ETH Network

Soft test network engineer preparation materials (1)

Strategy Pattern Abridged

WORKING WITH DATETIME IN PYTHON

Linux Privilege Escalation By Using SUID

Use Python to Scrape LinkedIn Profiles

Developer Tech News #4

Genuine Nissan OEM Cam Angle Sensor

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Mike Diaz

Mike Diaz

Solutions Engineer

More from Medium

Exception Handling in Java.!!!

The Java Library

Pattern matching for switch in java 17.

Garbage Collection — Java Architecture