Upload
others
View
1
Download
0
Embed Size (px)
Citation preview
Integration testing
1. Introduction
In this lab we will learn how to write integration tests. Sometimes the software to be tested has an
external component that the developer/tester has no control of (e.g. it’s not fully developed yet)
and it’s the developer’s (or QA’s) job to fully test the service without the existence of the other
service with which the tested service will interact with. In this lab we will learn how to mock non-
existing Java classes and ensure that the requests this service receives return the expected
result.
Useful vocabulary:
Integration testing is a software testing method where you test individual software
components or units of code to confirm proper interaction between various software
components.
Mock objects are objects that mimic the behaviour of a real objected in a controlled
manner.
Callback is any executable code that is passed as an argument to other code that is
expected to call back (execute) the argument at a given time. In plain English, a
callback function is like a Worker who "calls back" to his Manager when he has
completed a Task.
Mobile payment is a transaction that takes place through a mobile device. You can
take Google as an example. If you have Telia as your operator in Estonia, then you
can purchase content on Google Play without using a credit card. It can be done by
choosing Telia as your Payment method when purchasing content.
Operator is a mobile network operator. (e.g. Telia and Tele2)
2. Service to be tested: Operator Communicator
The service that is under testing is a mobile payment service. A user has given their consent to
a merchant (e.g. a gaming company) to charge the user, so they can receive extra content from
their product.
Summary of components involved in the flow:
User – a mobile phone user who wishes to purchase a product, but does not have a credit card.
In this case, he or she will purchase the product through their mobile phone operator.
Merchant – a person or a company that provides a digital service where users can purchase
digital goods.
Merchant Communicator – a microservice that handles communication between the merchant’s
backend and other microservices.
Operator Communicator – a microservice that handles communication between the Merchant
Communicator and the operator’s platform.
Operator – mobile network operator of the user.
Figure 1 shows business-side flows that are to be implemented in the service.
Figure 1. Business-side logic for Operator Communicator
Flow explanation:
Let’s say the user is playing a mobile game and wants extra paid content to enjoy it even more.
For that the user gives the developer of the game – the merchant – a request to bill his mobile
number to get the paid content. The merchant then continues to communicate with its partners
that handle all the mobile billing for them (which is basically us, the developer of the software to
be tested). First, the notification is sent to Merchant Communicator, which handles the
communication between the merchant and the Operator Communicator. The Operator
Communicator handles all communication done to the operator’s API. During the “success flow”,
the Operator tries to charge the user on their side and - since it is successful - it responds with
code “0”. Since the response given by the Operator is 0, we need to translate this response into
a string OK that our system can understand (see box “translates as ok”)”. Next, the Operator
Communicator tells the Merchant Communicator that the charge was successful, so it can notify
the merchant that the user has been charged and should receive the extra content they paid for.
However, if the Operator did not manage to charge the user (see Figure 1 red flow), it notifies
failure with the response code “1”. Operator Communicator translates this as a failure and the
merchant is notified that the user has not been charged and should not receive the extra content
they wished to buy.
The system under test (SUT) is Operator Communicator. Currently, it’s the only service along
with the Operator’s API that exists.
MerchantCommunicator and the Operator are components that we will mock. In case of the
Operator, it’s because it’s a third party component whereas the MerchantCommunicator is not
implemented yet.
In this lab we will first write tests for PaymentService, where we will mock the OperatorClient
and when creating tests for OperatorClient, we mock the Operator’s server.
3. Writing integration tests
Project setup
You are provided with a project that includes the implementation of the Operator Communicator
component. To understand the structure of the classes in the project, see Appendix 3.
1. Download the operator-communicator-student-version file from the lab page.
2. Make sure that you have JDK 8 and gradle plugin for IntelliJ (Tutorial:
https://www.jetbrains.org/intellij/sdk/docs/tutorials/build_system/prerequisites.html).
3. In IntelliJ: New -> Project From Existing Sources…
4. Pick the operator-communicator-student-version
5. Let gradle build your project
6. Test if everything works by running the tests (Right click on test folder and click Run ‘All
tests’
Figure 2. Technical flow of Operator Communicator
The flow (see figure 2) begins in PaymentService class with method
initiatePaymentToOperator().
What approach to take when mocking?
Starting with mocking is simple, but knowing what to mock can be quite difficult. Firstly, you have
to understand what mocking is and what possibilities it offers.
Mocking is a process used in unit testing where the unit under testing is dependant on external
components. It is used when we want to focus on testing the unit under testing and not the state
and behavior of the other components it depends on. In mocking, those dependencies are
replaced by controlled replacement objects that simulate the behavior of actual components.
Now that you’re somewhat sure what mocking is, I’m going to provide you with hints on what to
look out for when mocking.
A big hint: look for HTTP requests. Classes that do HTTP requests need to be mocked, because
we don’t want to do real requests against the API when we run tests - it would be problematic,
especially if it’s against an actual URL.
For example: OperatorClient’s only job is to do HTTP requests against the operator’s API,
therefore mocking the server should be the best choice for these types of tests – it’s only parsing
operator’s responses into the appropriate models. But PaymentService is only interested in the
values those models receive, therefore when().thenReturn() approach would be sufficient.
Look at the classes under testing and what methods they go through. Decide if we need to mock
anything internally that isn’t a simple constant.
You can test the two classes in one way, but however as we want to learn as much ways of
mocking in unit tests as possible, we will be covering two different ways of testing.
In this lab we will first write tests for PaymentService, where we will mock the OperatorClient and
when creating tests for OperatorClient, we mock the Operator’s server.
Testing with MockWebServer
You can find detailed information about MockWebServer here:
https://github.com/square/okhttp/tree/master/mockwebserver
MockWebServer mocks the behaviour of another web server. This will be used to mock the
Operator’s responses to our code and this will also help us assert that the correct values are sent
in the callback to the merchant.
Useful code snippets:
To initiate a MockWebServer:
MockWebServer mockWebServer = new MockWebServer();
In case you want to change the port of the server (ex. On port 9000):
mockWebServer.start(9000);
To mock the response of the MockWebServer:
mockWebServer.enqueue(new MockResponse().setBody(“Hello world!”));
NB! The response code of the HTTP request will be 200 by default. However if you
want to change that then you can set it by adding.setResponseCode(400) to the
MockResponse.
To close the mocked web server:
mockWebServer.shutdown();
Here is an example on how to write tests for Java classes which interact with external servers
via HTTP, with MockWebServer:
We first set up the @Before and @After. In @Before we also initiate the MockWebServer.
private MockWebServer operatorServer;
@Before
public void setUp() throws IOException {
this.operatorServer = new MockWebServer();
this.operatorServer.start(9000);
}
@After
public void tearDown() throws IOException {
this.operatorServer.shutdown();
}
We will also initiate a new OperatorClient object - the Java class that will be doing the actual
request.
final private OperatorClient client = new OperatorClient();
Then we queue the response the MockWebServer will return in JSON format using the enqueue
method. The response body will be set as {“code”: “0”}, because we know the Operator sends
parameter “code” containing the value “0” when the request succeeds (see Table 4) and that the
response body will be in JSON format.
Now we initiate the requestPaymentForUser() method in the client.
final OperatorResponse response = this.client.requestPaymentForUser(new
OperatorRequest());
Then it will be a successfully made into a model
Now we assert if the response is parsed into a model properly.
assertEquals("0", response.getCode());
Testing with Mockito and ArgumentCaptor
Read more here: https://javacodehouse.com/blog/mockito-tutorial/
With Mockito you can mock the object the mocked class will return to you, but remember that it’s
not good practice to mock everything, because if the changes are made in code, the tests will still
pass. It is good practice to mock anything that you have no control of.
Using when-then is almost self-explanatory. It tells the Mockito framework that when you reach
this line of code, then return this value instead and proceed.
Useful code snippets:
You can mock what a method of an object returns:
when(callback.getUrl()).thenReturn(“http://localhost:9000/callback”);
You can also mock that the method returns an exception:
when(callback.getUrl()).thenThrow(exception);
Here is an example on how you can test in various ways using Mockito and how to verify values
using ArgumentCaptor.
ArgumentCaptor is used to capture argumeny values for further assertions. For this case, we
want to assert that the object values passed to OperatorClient were as expected.
Example:
ArgumentCaptor<Person> varArgs = ArgumentCaptor.forClass(Person.class);
verify(mock).varArgMethod(varArgs.capture());
List expected = asList(new Person("John"), new Person("Jane"));
assertEquals(expected, varArgs.getAllValues());
Read more: https://static.javadoc.io/org.mockito/mockito-
core/2.6.9/org/mockito/ArgumentCaptor.html
Before we start to write the actual test cases, we have to prepare the test class to use Mockito
mocks.
First, we initiate the class rule. This is necessary so that the Mocks we want to use will be
initialised before each test method.
@Rule
public MockitoRule mockitoRule = MockitoJUnit.rule();
Next, we will inform the test class that we want to mock OperatorClient. The @Mock annotation
informs Mockito this will be a mocked object.
@Mock
private OperatorClient operatorClient;
Then we will create a new PaymentService object and use the @InjectMocks annotation, which
will create an instance of the class and every class that has a @Mock annotation (see
OperatorClient above) will be injected into this instance.
@InjectMocks
private PaymentService paymentService = new PaymentService();
From the Operator Communicator API provided above (see 2.1) we can see the parameters that
will initiate the PaymentService class initiatePaymentToOperator() method (see Table 1) and
what the possible returned end results are (see Table 2).
Based on this information, we will hard code the driver of the method that we will test. To make
the testing clear, we will create the object in a private method in the class.
private PaymentRequest generatePaymentRequest() {
final PaymentRequest request = new PaymentRequest();
request.setAmount("10");
request.setMerchantId("1");
request.setPaymentId("test_payment_id");
request.setUserPhoneNumber("552211994");
return request;
}
Now the preparations are done and we can begin writing actual tests.
The first thing we need to do in the test is to mock the response OperatorClient will return.
Instead of using MockWebServer to mock the response the operator will send, we mock the
result the OperatorClient should return whenever the request is successful.
For that we use Mockito’s methods when(<this mocked object initiates this method with
these values>).thenReturn(<this object instead of going into the method>).
Based on the information provided above (see 2.2 Table 4), we know that in a success case,
the value of “code” in the operator’s response should be “0”. We will hard code the object and
place it as parameter for the thenReturn() method.
private OperatorResponse generateSuccessfulResponse() {
OperatorResponse response = new OperatorResponse();
response.setCode("0");
return response;
}
when(operatorClient.requestPaymentForUser(any(OperatorRequest.class))
.thenReturn(this.generateSucceedResponse());
As you can see we put the Mockito method any(OperatorRequest.class) as a parameter for
the requestPaymentForUser() method. This means that it will provide thenReturn() when any
objects of OperatorRequest class are passed to the requestPaymentForUser() method.
Now we also know that we need to pass a PaymentRequest object into the
initiatePaymentToOperator() method to initiate it. We could add an empty object, but as we
want to assert if the values of the object OperatorRequest that are passed to the mocked
OperatorClient, we will use the generatePaymentRequest() method we created above during
test preparations.
We initiate the payment service and save the result as a separate object for assertions:
final PaymentResult result =
this.paymentService.initiatePaymentToOperator(this.generatePaymentRequest());
First, we assert that the response was OK as it’s the most critical for our service.
assertEquals(Constant.OK, result.getResult());
Then we want to assert if the OperatorRequest parameters were passed to the mocked object
as expected and if the values are correct. This is important as the Operator expects the values
to be in accordance to their API. If some values don’t reach the operator then the success flow
will fail in production.
Now we will learn how to use ArgumentCaptor.
First we need to create an ArgumentCaptor for OperatorRequest object, because we want to
assert what parameters were passed to the OperatorClient.
ArgumentCaptor<OperatorRequest> operatorRequestArgumentCaptor =
ArgumentCaptor.forClass(OperatorRequest.class);
Next we use Mockito’s verify() method to verify if the method for this mocked object was called
and assign the ArgumentCaptor we created along with the method call capture() as a
parameter for requestPaymentForUser(). This means that the ArgumentCaptor will fetch any
objects that are passed where it’s assigned.
verify(operatorClient).requestPaymentForUser(operatorRequestArgumentCaptor.capture());
Now we can check the values of the object the ArgumentCaptor caught. To get the object we
use the getValue() method.
assertEquals("10",operatorRequestArgumentCaptor.getValue().getAmount());
assertEquals("test_payment_id",
operatorRequestArgumentCaptor.getValue().getPartnerId());
assertEquals("552211994",
operatorRequestArgumentCaptor.getValue().getUserNumber());
To write tests, you have to understand what values are needed to create a request and a
response. Usually when integrating with other APIs, you are provided with an API document
(see Appendix 1 and 2). You need to take them into account (especially the types of the
parameters) when creating a request to the operator.
4. Additional exercises for students
1. Write a failure case for OperatorClient using MockWebServer
2. Write a failure case for PaymentService using Mockito
Appendix 1. Operator Communicator API request and response parameters
For this SUT, the Merchant Communicator sends the following parameters, which are to be used
for generating a payment request to the Operator:
Table 1. Request parameters received by Operator Communicator (PaymentService class) from
Merchant Communicator
Parameter name Parameter type Explanation Example value
paymentId String Unique id for the
payment process
1
merchantId String Merchant specific
unique id
0001
userPhoneNumber String User phone number
that will be charged
552211994
amount String The amount the user
needs to be charged
10
In return the following response from the Operator Communicator is expected by Merchant
Communicator:
Table 2. Response parameters from Operator Communicator (PaymentService class) to
Merchant Communicator
Parameter name Parameter type Explanation Example values
result String The result of the
payment request.
If the charge was
successful then it
should be given the
value “OK”, otherwise
it should be given the
value “failed”. No
other results are
expected.
Appendix 2. Operator API request and response parameters
Here are the required parameters for the request:
Table 3. Request parameters from the Operator Communicator received by the Operator
Parameter name Parameter type Explanation Example values
amount String The amount the user
needs to be charged
10
partnerPaymentId String Partner specific
unique id. The partner
is us, who will be the
communication line
between the
merchant and the
operator.
0cf833c6-4ed1-
11e9-8647-
d663bd873d93
userPhoneNumber String The number that will
be charged.
556677889
Here is the response the operator will send:
Table 4. Response parameters from the Operator sent to the Operator Communicator
Parameter name Parameter type Explanation Example values
code String The result of the
charge
If the user was
successfully charged,
then the operator will
return “0”, and if it
failed it will return “1”
NB! During the writing of the test, you may use custom values or values provided in the example
values below, but operator responses should never be custom as it’s part of a component that
isn’t developed by us.
Appendix 3. Structure of classes
Http
OperatorClient
A class that directly communicates with the operator’s API via HTTP
request.
It parses the JSON request body into JSON format and forwards the
information to the operator’s endpoint.
Model
Operator
OperatorRequest
Model for the request parameters that the operator will require.
OperatorResponse
Model for the response parameters that the operator potentially
sends.
Payment
PaymentRequest
Model for the request parameters that reach the Operator
Communicator.
PaymentResponse
Model for the response parameters that reach the Operator
Communicator.
Service
PaymentService
The most important part of the flow, where requests and responses for
the mobile payment are handled.
Request is built and sent to the OperatorClient class, which
communicates with the operator.
Returns OK, when the operator responds with “0” as in the request was
successful and it translates everything else as FAILED.
Util
Constant
Contains constants that we frequently use