Files
apophis-fastify/no_commit_paper.md
T

2170 lines
145 KiB
Markdown
Raw Normal View History

2026-05-20 16:09:43 -07:00
Ana Catarina Malhado Ribeiro
MSc Student
Invariant-Driven Automated Testing
Dissertation submitted in partial fulfillment
of the requirements for the degree of
Master of Science in
Computer Science and Informatics Engineering
Adviser: Carla Ferreira, Associate Professor,
NOVA University of Lisbon
Examination Committee
Chairperson: António Ravara, Associate Professor, NOVA University of Lisbon
Raporteur: Jácome Cunha, Assistant Professor, University of Minho
Member: Carla Ferreira, Associate Professor, NOVA University of Lisbon
February, 2021
arXiv:2602.23922v1 [cs.SE] 27 Feb 2026
Invariant-Driven Automated Testing
Copyright © Ana Catarina Malhado Ribeiro, Faculty of Sciences and Technology, NOVA
University of Lisbon.
The Faculty of Sciences and Technology and the NOVA University of Lisbon have the
right, perpetual and without geographical boundaries, to file and publish this dissertation
through printed copies reproduced on paper or on digital form, or by any other means
known or that may be invented, and to disseminate through scientific repositories and
admit its copying and distribution for non-commercial, educational or research purposes,
as long as credit is given to the author and editor.
This document was created using the (pdf)LATEX processor, based in the “novathesis” template[1], developed at the Dep. Informática of FCT-NOVA [2].
[1] https://github.com/joaomlourenco/novathesis [2] http://www.di.fct.unl.pt
Acknowledgements
First and foremost I would like to express my gratitude towards FCT Fundação para a
Ciencia e Tecnologia which grant support this works development. I would also like to
thank my adviser, Carla Ferreira, whose consistent help was determinant for this works
success.
To my friends, Danna Krupka, André Rodrigues and Dymytry Krupka. Thank you for
keeping me sane when all hell broke lose. To my friends on the other side of the globe,
Maddalena Menabue and Matteo Doria, thank you for making my days a joy.
To my parents, which always make the impossible come true. This wouldnt be possible without your unconditional support.
Finally I would like to thank my brother for believing in me even when I didnt.
v
If we knew what it was we were doing, it would not be called
research, would it?
Abstract
Microservice architectures are an emergent technology that builds business logic into
a suite of small services. Each microservice runs in its process and the communication is
made through lightweight mechanisms, usually HTTP resource API. These architectures
are built upon independently deployable and, supposedly, reliable pieces of software that
may, or may not, have been developed by the team using it. Nowadays, industries are
dangerously migrating into microservice architectures without an effective and automatic
process for testing the software being used. Furthermore, current API specification languages are not expressive enough to be used for testing purposes. To solve this problem
it is necessary to extend currently broadly used API specification languages. APOSTL is
a specification language to annotate APIs specifications based on first-order logic, with
some restrictions. It has the purpose of extending the currently used API description
languages with properties that can be useful for testing purposes, transforming these description documents into useful testing artifacts. Besides providing information needed
for testing an application, APOSTL also provides an API with semantic. This additional
information is then leveraged to automate microservice testing.
The work developed in this thesis aims to fully automate the microservice testing
process. It is achieved by the implementation of PETIT a tool able to test microservices
when provided with an OpenAPI Specification document, written in JSON and properly
annotated with the previously proposed specification language, APOSTL.
The tool is able to analyze microservices independently from the source code availability.
Keywords: automated testing, microservices, black-box testing, design by contract, test
data generation
ix
Resumo
As arquitecturas de microserviços são uma tecnologia emergente que constrói lógica
empresarial através de um aglomerado de pequenos serviços, onde cada um deles corre
num processo independente e a comunicação é feita a partir de mecanismos de comunicação leves, usualmente HTTP com APIs para recursos. Estas arquitecturas são construídas
com base em software desenvolvido de forma independente, supostamente fiável, e que
pode, ou não, ter sido desenvolvido pela mesma equipa que o utiliza. Actualmente, a
indústria está a migrar, de forma perigosa, para arquitecturas de microserviços sem que
exista um processo automatizado e eficiente para testar o software que estão a utilizar.
Além disto, as linguagens de descrição de APIs actualmente utilizadas não são suficientemente expressivas para serem usadas para fins de teste. Para resolver este problema, é
necessário extender as linguages de descrição de APIs mais utilizadas. APOSTL é uma
linguagem de especificação para anotar descrições de APIs, baseada em lógica de primeira
ordem. Tem como propósito extender linguagens de descrição de APIs com propriedades
úteis para fins de teste, transformando os documentos de descrição em artefactos de teste
úteis. Para além de fornecer informação útil para fins de teste, a APOSTL também dota
a API com semântica. Esta informação adicional pode ser utilizada para automatizar o
processo de teste de microserviços.
O trabalho desenvolvido nesta tese ambiciona automatizar totalmente o processo de
teste de microserviços. Este objectivo é atingido com a implementação da PETIT, uma
ferramenta capaz de testar microserviços apenas com a sua especificação, escrita em JSON,
e devidamente anotada com fórmulas em APOSTL.
A ferramenta de teste desenvolvida é capaz de analizar microserviços independentemente da disponibilidade do código fonte.
Palavras-chave: teste automatizado, microserviços, testes de caixa-negra, desenho por
contracto, geração de dados de teste
xi
Contents
List of Figures xv
List of Tables xvii
Listings xix
1 Introduction 1
1.1 Context . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.2 Motivation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.3 Proposed Solution . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.4 Contributions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.5 Document Structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
2 Background 5
2.1 Program Verification . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
2.2 Hoares Logic . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
2.3 Design by Contract . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.4 Software Testing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
2.4.1 White-Box Testing . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.4.2 Black-Box Testing . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.5 Microservices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.5.1 Service-Oriented Architecture . . . . . . . . . . . . . . . . . . . . . 10
2.5.2 Microservice Architecture . . . . . . . . . . . . . . . . . . . . . . . 10
2.5.3 OpenAPI Specification . . . . . . . . . . . . . . . . . . . . . . . . . 11
3 Related Work 17
3.1 Black-Box Testing Techniques . . . . . . . . . . . . . . . . . . . . . . . . . 17
3.1.1 Random Testing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
3.1.2 Specification-Based Testing . . . . . . . . . . . . . . . . . . . . . . 18
3.1.3 Learning-Based Testing . . . . . . . . . . . . . . . . . . . . . . . . . 18
3.1.4 Adaptive Random Testing . . . . . . . . . . . . . . . . . . . . . . . 19
3.1.5 Discussion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
3.2 Tools for Automated Testing . . . . . . . . . . . . . . . . . . . . . . . . . . 21
xiii
CONTENTS
3.2.1 QuickCheck . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
3.2.2 JET . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
3.2.3 Korat . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
3.2.4 Discussion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
3.3 Extending OpenAPI: HeadREST . . . . . . . . . . . . . . . . . . . . . . . . 24
3.4 Current Industrial Practices . . . . . . . . . . . . . . . . . . . . . . . . . . 25
3.4.1 Manual Testing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
3.4.2 Semi-Automated Testing . . . . . . . . . . . . . . . . . . . . . . . . 25
4 Solution Design 27
4.1 Tournaments Application . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
4.2 Specification Language: APOSTL . . . . . . . . . . . . . . . . . . . . . . . 30
4.2.1 Data Generation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
4.3 Testing Tool: PETIT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
5 Solution Implementation 37
5.1 Specification Language: APOSTL . . . . . . . . . . . . . . . . . . . . . . . 37
5.1.1 Extending OpenAPI Specification . . . . . . . . . . . . . . . . . . . 37
5.1.2 Grammar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
5.1.3 Integration with PETIT . . . . . . . . . . . . . . . . . . . . . . . . . 40
5.1.4 Restrictions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
5.2 Testing Tool: PETIT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
5.2.1 Architecture Components . . . . . . . . . . . . . . . . . . . . . . . 42
5.2.2 Testing Process . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
6 Evaluation 49
6.1 Testing Constructors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
6.2 Testing Mutators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
6.3 Testing Observers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
6.4 Tournaments Application: faulty scenario . . . . . . . . . . . . . . . . . . 57
7 Conclusions and Future Work 61
7.1 Conclusions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
7.2 Future Work . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
References 63
Online references 67
xiv
List of Figures
2.1 Pet store API example. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
2.2 Operation POST expanded. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
4.1 Steps needed to execute PETIT. . . . . . . . . . . . . . . . . . . . . . . . . . . 28
4.2 Player schema from tournaments application. . . . . . . . . . . . . . . . . . . 29
4.3 Tournament schema from tournaments application. . . . . . . . . . . . . . . 30
4.4 Players API operations. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
4.5 Tournaments API operations. . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
4.6 PETITs architecture. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
5.1 Parse tree of a conforming APOSTL formula. . . . . . . . . . . . . . . . . . . 40
5.2 Generate operation logic. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
5.3 Generate body schema operation logic. . . . . . . . . . . . . . . . . . . . . . . 44
5.4 Generate URL parameter operation logic. . . . . . . . . . . . . . . . . . . . . 44
xv
List of Tables
4.1 Operation test outcomes. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
5.1 APOSTLs grammar defined in BNF. . . . . . . . . . . . . . . . . . . . . . . . . 39
6.1 Error detection in each order strategy. . . . . . . . . . . . . . . . . . . . . . . 59
xvii
Listings
2.1 YAML object for the API information description. . . . . . . . . . . . . . . 13
2.2 YAML object for the API servers. . . . . . . . . . . . . . . . . . . . . . . . 13
2.3 YAML object for the API servers. . . . . . . . . . . . . . . . . . . . . . . . 13
2.4 YAML object for the API servers . . . . . . . . . . . . . . . . . . . . . . . . 14
4.1 Players API POST player operation contract. . . . . . . . . . . . . . . . . 32
4.2 Players API DELETE player operation contract. . . . . . . . . . . . . . . . 32
4.3 Tournaments API invariant. . . . . . . . . . . . . . . . . . . . . . . . . . . 32
4.4 YAML object for Players API get player operation. . . . . . . . . . . . . . 33
4.5 Error message when operation order strategy is wrongly specified. . . . . 35
4.6 PETITs output when testing an API with a single operation. . . . . . . . 36
4.7 PETITs output when testing an API with a single operation. . . . . . . . 36
5.1 YAML object for Players API delete player operation. . . . . . . . . . . . 38
5.2 YAML object for Tournaments API. . . . . . . . . . . . . . . . . . . . . . . 38
5.3 A nested quantifier, written in APOSTL. . . . . . . . . . . . . . . . . . . . 41
5.4 A quantifier with more than one variable, written in APOSTL. . . . . . . 41
5.5 An invalid block parameter in an APOSTLs formula, according to its implementation. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
6.1 Specification test results when executing PETIT with COM order strategy. 50
6.2 PETITs partial output of a tournaments API test executed with COM
strategy. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
6.3 Specification test results when executing PETIT with CMO order strategy. 52
6.4 PETITs partial output of a tournaments API test executed with CMO
strategy. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
6.5 PETITs partial output of a players API test executed with MCO strategy. 54
6.6 PETITs partial output of a tournaments API test executed with MCO
strategy. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
6.7 Specification test results when executing PETIT with MOC order strategy. 55
6.8 YAML partial object for Players API get player operation. . . . . . . . . . 56
6.9 YAML partial object for Tournaments API get tournament operation. . . 56
6.10 PETITs test results for the faulty player insertion. . . . . . . . . . . . . . 57
6.11 PETITs test results for the faulty player deletion. . . . . . . . . . . . . . . 58
xix
C h a p t e r
1
Introduction
This chapter presents the context for the problem as well as the motivation to solve it.
It also briefly describes the implemented solution, this works contributions and a brief
description of this documents structure.
1.1 Context
Microservice architectures are an emergent technology that builds business logic into
a suite of small services, each running in its own process and communicating through
lightweight mechanisms, usually HTTP resource API.
Microservices code can be hidden to client applications which makes them black-box
systems. In order to test such systems, one needs access to its specification. Current API
specification languages have only information about the types, e.g., the operation responsible for adding a pet has in its specification information about what should be carried in
the request the representation of the new pet (name, photo, owner information) , and
information about the response contents, typically, an HTTP code according to the operation success or failure. This information is not enough to meaningfully and efficiently
test microservices. In order to test such systems, it is necessary to know which properties
should be guaranteed before and after an action call. Current API specification languages
are not expressive enough to be able to provide these kind of properties invariants, pre
and postconditions. Thus, beyond the need for an efficient method to test microservices,
there is the need for extending current API specification languages in order to be able
to specify these logical conditions. In the previous example, one possible precondition
could be that a request made to obtain a pet given its identifier should respond with the
HTTP code 404 (not found); one possible postcondition could be that making a request to
obtain a pet with the same inserted identifier should respond with the previously inserted
1
CHAPTER 1. INTRODUCTION
pet object.
1.2 Motivation
Nowadays, industries are dangerously migrating into microservice architectures without
an effective and automatic process for testing the software being used. Microservice
architectures are built upon independently deployable and, supposedly, reliable pieces
of software that may, or may not, have been developed by the team using it. How can
one, effectively, test such services if the code is not accessible? The current practices of
testing microservices consist of manually producing requests and checking the requests
responses and, therefore, are not reliable. Hence, the motivation behind this thesis lies
on the fact that there is no trustworthy automatic process for testing microservices as a
black-box.
The current way of specifying microservices APIs are not suitable to testing, meaning
APIs contain little to no information that aids in the microservice testing process. Thus,
there is also a demand to develop an extension to current API specification languages in
order to add useful information that can improve testing results.
This thesis problem can be approached in two different, equally useful, ways: the first,
and more obvious, testing microservices as a black-box, not having access to its code; the
second, verifying if a given microservice implementation diverges from its specification.
1.3 Proposed Solution
In this thesis it is proposed a new methodology for automatically testing microservices
having only access to its API description. The developed tool, PETIT aPi tEsTIngTool
, is able to test microservices when provided with an OpenAPI specification document,
written in JSON, properly annotated with the proposed specification language, APOSTL
API PrOperty SpecificaTion Language. These annotations consist mainly, but not exclusively, of invariants, pre and postconditions written at the cost of the same APIs
operations.
Besides making requests to the API and evaluating the obtained results, PETIT is
also able to generate the test data that is used to perform the tests and evaluate whether
an API or an API operation is, in fact, according to its specification. As such, PETIT
is composed by a parser to parse the OpenAPI Specification document , an input
generator responsible for all test data generations , an APOSTL formula parser to
check whether an APOSTL formula is according to its grammar , an HTTP manager
component responsible for managing all HTTP interactions between PETIT and the
microservice being tested , and, finally, the tester and evaluator component which,
as the name suggests, is responsible for the testing, so to speak, and for the formulas
evaluation.
2
1.4. CONTRIBUTIONS
In short, PETIT generates input, performs requests to the specified operations and,
finally, evaluates the obtained results.
1.4 Contributions
This work contributions are an API specification language developed to specify API
contracts, and an algorithm which automatically generates, meaningful, not redundant,
test data to test microservices, based on its extended specification.
The specification language adds invariants, pre and postconditions to an already
existing API description. The developed specification language lacks expressiveness
when compared to others, e.g., HeadREST [1]. However, the fact that the specification
is built from API pure operations makes it easier to use and understand. Using the
operations from the API itself makes the specification closer to what programmers are
used to write, thus, gaining in terms of usability.
A tool is developed to integrate the test case generation algorithm with the ability
to automatically make requests to microservices, and check if the obtained response is
verified by the oracle. The tool provides the user with the ability to test several APIs
at once as long as they are specified in the same document to study the interactions
between them. The operations are divided into three categories constructors, observers,
and mutators. The operation order within each category is selected randomly at the
beginning of each execution. The user has the ability to control the order in which these
categories are being tested, as well as the granularity of the output produced by the tool.
In short, the main contributions are an API description language, and a tool that fully
automates the process of testing microservices, given a microservice specification.
1.5 Document Structure
The remaining of this document is organised as follows:
Chapter 2 - Background provides information on key concepts necessary to understand
this works development, more precisely, software testing techniques white and
black-box testing , what are microservices and from what they evolved from, and
an example of an API description language OpenAPI Specification.
Chapter 3 - Related Work besides presenting some tools that automate softwares testing process, this chapter also introduces relevant black-box testing techniques that
can be applied to this thesis problem.
Chapter 4 - Solution Design describes the design process for both PETIT and APOSTL.
It also illustrates how to use PETIT and APOSTL with an example tournaments
application. This chapter also describes PETITs architecture and all its possible
outcomes.
3
CHAPTER 1. INTRODUCTION
Chapter 5 - Solution Implementation describes how PETIT and APOSTL are implemented.
This chapter is compartmentalized in two sections, the first being responsible for
APOSTLs implementation, and the second for PETITs implementation. As such,
the first section provides insight on how APOSTL is integrated with OpenAPI Specification, and a formal definition of APOSTLs grammar. The second, provides
information on the testing methodology implemented by PETIT, and a description
of all its architectural components.
Chapter 6 - Evaluation analyses PETITs tests results when testing a correct implementation of the tournaments application, as well as a faulty one. Implementation
errors are incrementally added in order to ascertain if PETIT finds them and, if it
does, how useful is its output.
Chapter 7 - Conclusions and Future Work provides this works conclusions and presents
what can be improved in both PETIT and APOSTL.
4
C h a p t e r
2
Background
This chapter presents essential topics that aid in the comprehension of this thesis subject
invariant-driven automated testing applied to microservices. The first section describes
program verification; next, there is a description of Hoares logic, which is essential
to understand programs specifications; it also explains what is design by contract, an
approach to software design. Software testing section includes a brief introduction to
different testing strategies: black-box and white-box testing. The following section aims
to explain what are microservice architectures as well as service-oriented architectures,
where both these concepts came from, their necessity and why microservices popularity
is rising. Hereupon, this section aims to explain what is software testing as well as what
is, in this case, the software under test microservices.
2.1 Program Verification
Being able to formally guarantee a programs correctness has been a constant problem
during software development. To tackle this, it was necessary to develop some way of
describing a programs expected behaviour: a program specification. Although this might
seem a good idea, writing correct specifications is not easy and not always adopted by developers: besides having to write the program, they also have to reason about all possible
correct program states and describe them. This results in incomplete specifications that
might not match the written program nor guarantee its correctness.
To solve this problem the concept of program analysis arises. A program can be analysed statically or dynamically. If the analysis is static, it happens at compile time based
on the programs source code meaning the program is not executed. This guarantees
that if the program satisfies a property, then all its executions will satisfy that same property. Static analysis finds weaknesses in an early stage of development, resulting in less
5
CHAPTER 2. BACKGROUND
expensive fixes. If the program analysis happens to be dynamic, the program is executed
against a set of test cases. It is extremely important to choose an adequate set of test cases:
the test set should test as many different program states as possible. If test cases follow
this rule, dynamic analysis can be considered more effective than static analysis.
Although both analysis approaches can be performed independently, the most effective way of analysing a program is to combine them: a static analysis should be performed
followed by a dynamic analysis. On one hand, defects such as unreachable code, undeclared (or unused) variables, and uncalled functions are not detected in dynamic analysis.
On the other hand, static analysis can produce false positives by, e.g., taking into account
a condition that may never be true.
This thesis lies on dynamic program analysis, since its purpose is to automate microservice testing.
2.2 Hoares Logic
Hoares logic was first introduced by Hoare in 1969 [2] with the purpose of providing a
logical basis for proofs of the properties of a program, e.g., the most important property
of a program is whether it carries out its intended goal. This goal can be specified by
making general assertions on the relevant variables values, after the programs execution
rather than specifying particular values, assertions describe general values properties
and relationships between them.
Hoare also states that the validity of a programs outcome depends on the values taken
by the variables before the program is initiated. This means one can also define assertions
in the same way as the ones used to describe the results obtained upon termination.
Hence, a new notation was introduced to connect precondition properties P, program
execution Q and properties describing the expected results R:
P {Q} R
This notation can be interpreted as “if the assertion P is true before initiation of
a program Q, then the assertion R will be true on its completion” [2]. Assuming the
absence of side effects on the evaluation of expressions and conditions, Hoare described
the following axiom and rules:
1. Axiom of Assignment
Considering the assignment x B f , if any assertion P (x) is true after the assignment,
it must also be true on the value of f before the assignment, i.e., P (f ) must also be
true before the assignment.
2. Rules of Consequence
If the execution of a program Q ensures the truth of assertion R, then it also ensures
the truth of every assertion logically implied by R [2]. Moreover, the same is applied
6
2.3. DESIGN BY CONTRACT
to precondition properties: if Qs execution ensures the truthiness of P , then it also
ensures that every assertion logically equivalent to P is true.
3. Rule of Composition
A program is a sequence of statements executed one after another. Thus, a program
Q can be defined as the sequence of all its n statements: Q = (Q1; Q2; Q3; ... ; Qn).
In formal terms, the rule of composition is:
IF P {Q1} R1 AND R1 {Q2} R
THEN P {(Q1; Q2)} R
This means that if the resulting outcome of executing Q1 satisfies Q2s precondition, and Q2 satisfies the final outcome condition R, then the whole program Q
sequence of Q1 and Q2 will produce the intended result.
4. Rule of Iteration
Considering the program Q = while B do S, the rule of iteration can be defined as
follows:
IF P AND B{S} P
THEN P {while B do S} ¬B AND P
P is a property that must be true on the loops life cycle, i.e., before entering the
loop, in all its iterations and on loops completion. B is the loops entering condition,
meaning that if B holds, then S is executed, otherwise the loop terminates. Thus, B
is assumed true upon initiation of the loop and false upon the loops completion.
Although the described rules can be used to construct the proof of properties of simple
programs, they are not sufficient to prove that a program terminates, e.g. as a result of
an infinite loop. Hence, P {Q} R should be interpreted as “provided that the program
terminates, the properties of its results are described by R” [2].
2.3 Design by Contract
Design by contract, applied to object-oriented architectures, was first introduced by Meyer
[3] with the goal of improving software reliability, which can be defined as the combination of correctness and robustness, i.e., the absence of bugs. The concept of reliable
software is often associated with defensive programming techniques, where the programmer wraps its code with as many checks as possible, even if they are redundant. Although
this technique may prevent some disasters, it can also cause new ones: introducing redundant code is never a good idea, either because it makes the code harder to understand,
or because new bugs are directly introduced in the new checks. Thereby, guaranteeing
7
CHAPTER 2. BACKGROUND
software reliability requires a more systematic approach, thus, arising the notion of design
by contract.
Inspired by the work on program proving and systematic program construction of
Hoare [2], Floyd [4] and Dijkstra [5], Meyer created the notion of contract based on contracts performed in modern society where both parts, the contractor and the client, have
obligations and benefits. Furthermore, an obligation for one of the parties is a benefit for
the other. Applying this concept to software development is straightforward: if the execution of a task depends on a routine call to handle a subtask, the relationship between
the client routine (the caller) and the called routine (the supplier) needs to be specified.
These relationships are specified through assertions predicates that can be:
Preconditions are applied to individual routines. Preconditions describe the state in
which the program must be before the call of a routine. If a precondition does not
hold, the client code violated the contract, and the effect of the called routine is
undefined and may, or may not, carry its intended purpose. If no precondition is
specified or the predicate is true , all program states are accepted.
Postconditions are applied to individual routines. Postconditions describe the state of
the program after the routine call. If a postcondition is violated, the supplier code
has a bug, thus violating the contract. If no postcondition is specified, all program
states are accepted after the routines execution.
Invariants constraint all the routines of a class. Invariants are properties that must ever
hold, in any circumstance. Hence, it must hold upon the creation of a class instance,
and hold before and after every execution of every routine the class offers.
Assertions do not aim to specify special cases. Instead, they specify expected cases.
Special cases should be handled through standard conditional control structures, e.g., if
statements.
Pre and postconditions “strength” should be carefully thought. While strong preconditions put a burden on the client side, weak ones are a burden in the supplier code.
Choosing between the two is a matter of preference, though the key criterion should be
to always minimize architectures complexity.
2.4 Software Testing
According to Myers et al. [6], “testing is the process of executing a program with the
intent of finding errors” and “an unsuccessful test case is one that causes a program to
produce the correct result without finding any errors”.
According to Fowler [30], software developers should write self-testing code, so that
the testing process should be fully automated. Developers should create a test suite
that can be automatically run against the code to be tested. The test suite should be
built in such way that when all tests pass, one should be confident enough to release the
8
2.4. SOFTWARE TESTING
software to production. Hereupon, theres a necessity of defining rigorous methodologies
to automatically generate trustworthy test suites that can be also executed automatically.
Software testing can be compartmentalized in two main strategies: white-box testing
and black-box testing. There are several methodologies that follow each strategy and
wouldnt be realistic to approach all of them in this document. Thus, a few representative
ones were chosen. Both strategies and methodologies are discussed in detail on the
following subsections.
Complete test coverage is, generally, impossible to achieve. This affirmation is properly justified in the following sections.
2.4.1 White-Box Testing
White-box or logic-driven is a testing strategy where the software tester can go through
the subject programs implementation. Therefore, the test cases are derived from the
programs logic [7].
Hypothetically, achieving complete test coverage with a white-box testing strategy
should be through exhaustive path testing, which derives a control flow graph from the
implementation and then aims to build a test battery that executes all possible control
flow paths. Although all the paths are covered, one cannot conclude the program is
completely tested either because exhaustive path testing does not guarantee the program
matches its specification, the program might have missing paths, and covering all paths
does not check for data-sensitive errors.
Since the focus of this thesis is on automated testing of microservices from its specification, white-box testing techniques will not be further explored. More information on
the subject can be found in the survey by Anand et al. [8].
2.4.2 Black-Box Testing
Black-box testing, also known as input/output-driven testing [7], is a testing strategy where
the software tester is completely unaware of the programs implementation: its internal
behaviour and structure are unknown. Instead, the tester will have to derive test data
only from the programs specification.
Achieving complete test coverage using a black-box testing strategy implies that the
program should be tested with not only all values in the input domain but also with all
possible inputs. Testing following such criterion exhaustive input testing can produce
an infinite number of test cases thus, becoming impossible to achieve in an acceptable
time period.
In the following chapter some black-box testing techniques are introduced, since
theyre the ones applicable to this thesis subject.
9
CHAPTER 2. BACKGROUND
2.5 Microservices
In order to explain why, nowadays, microservice architectures are preferred over serviceoriented architectures, it is necessary to give a step back and understand why the need of
a different architecture arose in the first place.
In this section there is a brief explanation on how these software paradigms emerged
as well as definitions of their core components. Since both services and microservices are
available through APIs, this section also features OpenAPI, a standard for API descriptions.
2.5.1 Service-Oriented Architecture
According to Shadija et al. [9], in a service-oriented architecture a service is an entity,
accessible through an interface (API), encapsulating various components to provide an
individual business function. Furthermore, a component can be a service if its wrapped
by a service layer.
The notion of component emerged when object-oriented architecture was not enough
to fulfill the rising need of working at a higher level of granularity, i.e., having more
functionality into a single, independently replaceable and upgradeable entity [31]. As
such, component-based system development was the next big thing where systems were
composed by components and these consisted of several objects enclosed together.
In a service-oriented architecture services are connected through a robust and heavy
mechanism called Enterprise Service Bus (ESB) [9]. In spite of its robustness, this structure constraints the scalability of applications according to the business needs. For this
reason, service-oriented architectures hamper the evolutionary design of applications
and, once more, a need for a change of paradigm arises.
2.5.2 Microservice Architecture
Fowler [31] describes a microservice architecture as being the development of applications “as a suite of small services, each running in its own process and communicating
with lightweight mechanisms, often an HTTP resource API”. However, as the name suggests, shouldnt microservices be small portions of software? Not necessarily. According
to Shadija et al. [9], the granularity of a microservice is an important part of the architecture. Furthermore, having fine grained microservices can introduce an overhead on
managing the whole application. Hence, microservices are not necessarily small portions
of software, as the name wrongly suggests.
The microservice architecture contrasts with more conservative forms of software
development in the sense that a traditional application has all its functionality into one
process and, as needed, it scales by replication into several servers. On the other hand,
an application built according to a microservice architecture has its functionality spread
10
2.5. MICROSERVICES
into multiple services and it scales by replicating only the needed functionalities on a
server [31].
The motivation behind the creation of microservices was mainly scalability. A microservice architecture specifies end points with the associated business logic [9]. Microservices and client applications communicate through Hyper-Text Transfer Protocol
(HTTP) request-response via well specified endpoints on the microservice API. By using
sophisticated endpoints, microservices are able to adapt to the needs of an ever-growing
business logic. Since the application architecture is decentralized and the communication between microservices is cheap and easy, more logic can be implemented within
microservices.
The microservice architecture aims to build decoupled and modular applications.
Rather than using a complex communicating systems like an enterprise service bus, microservice developers prefer the approach “smart end points and dumb pipes”, i.e., having
a simpler middleware architecture and communicating through HTTP request-response
with resource APIs and lightweight messaging [31].
2.5.3 OpenAPI Specification
Representational State Transfer (REST) is an architectural style to develop web services.
Its nuclear concept are resources. To identify resources involved in component interactions, REST uses a resource identifier [1]. Since resources can be accessed and modified
concurrently through various components, a resource representation is used to capture the
current, or intended, state of that resource. Those representations are then transferred
between components through REST interactions. REST systems communicate over HTTP
and are made available to other systems as web resources identified by URIs [1]. Since the
communication is through HTTP, the interactions are all HTTP verbs: GET, POST, PUT
and DELETE to retrieve, add, update or remove resources. Additional information can
be sent in the headers and the body of an HTTP request, and the results always include a
response as well as a response status code.
RESTful systems are the ones developed using the REST architecture. These systems
are an agglomerate of resources and their respective actions. A RESTful API is a set of
resource identifiers as well as all the actions that can be performed on each resource.
OpenAPI Specification (OAS), formerly Swagger Specification [32], was created with
the purpose of standardizing the way RESTful web services are described. OpenAPI
is a description format for services APIs that is language independent, portable and
open [33]. Figure 2.1 contains an OpenAPI description of a pet stores pet management
system found in [34]. It shows four actions that can be performed, their URI and a textual
description.
11
CHAPTER 2. BACKGROUND
Figure 2.1: Pet store API example.
Figure 2.2 shows all information OAS provides for each operation. In this example,
operation POST in the URL “/pet” expects to receive a JavaScript object representing a
pet as parameter, and returns the HTTP code 405 in case of receiving an invalid input.
Figure 2.2: Operation POST expanded.
Although OAS files can be written in JSON or YAML, all examples will be presented
in YAML for readability purposes. An OpenAPI specification file has the following structure [35]:
12
2.5. MICROSERVICES
Information 2.1 contains the APIs current version, its title and all applicable licenses.
1 info:
2 version: 1 . 0 . 0
3 t i t l e : Swagger P e t s t o r e
4 l i c e n s e :
5 name: MIT
Listing 2.1: YAML object for the API information description.
Servers 2.2 have information on all API servers and their URLs. Different servers can
be used to implement an API, e.g. a sandbox server can be used with test data.
1 s e r v e r s :
2 - url: http:// p e t s t o r e . swagger . io /v1
Listing 2.2: YAML object for the API servers.
Paths 2.3 defines API endpoints. Each endpoint is comprised of all HTTP methods
it supports. Since each endpoint can be associated with different operations, the
definition of each operation is achieved by using a Path Item object which, in turn,
and depending on the HTTP method, has the summary, parameters array, request
body, and the responses array.
1 paths:
2 / pets / { petId }:
3 get:
4 summary: Info f or a s p e c i f i c pet
5 parameters:
6 - name: petId
7 in: path
8 required: true
9 d e s c r i p t i o n: The id of the pet to r e t r i e v e
10 schema:
11 type: s t r i n g
12 responses :
13 200:
14 d e s c r i p t i o n: Expected response to a valid request
15 content:
16 a p p l i c a t i o n / json:
17 schema:
18 $ r e f: "#/components/schemas/Pet"
19 default:
20 d e s c r i p t i o n: unexpected e r r o r
21 content:
13
CHAPTER 2. BACKGROUND
22 a p p l i c a t i o n / json:
23 schema:
24 $ r e f: "#/components/schemas/Error"
Listing 2.3: YAML object for the API servers.
Components 2.4 to condense the file size and avoid information repetition, the components section is where the data structures used throughout the API are defined.
Within components schemas can be defined. A schema has a type an array of
properties and an array indicating the required properties. Schemas are referenced
throughout the OAS document using the keyword $ref.
1 components:
2 schemas:
3 Pet:
4 type: o b j e c t
5 required:
6 - id
7 - name
8 p r o p e r t i e s :
9 id:
10 type: i n t e g e r
11 format: int64
12 name:
13 type: s t r i n g
14 tag:
15 type: s t r i n g
Listing 2.4: YAML object for the API servers
OAS does not have any information on the state of the system prior nor post operation
execution. However, it supports the addition of custom properties. By using this mechanism, it is possible to extend OAS in order to add information about the valid states in
which the system will perform as expected, as well as all information required to generate valid testing data. Hence, the addition of new properties, i.e. extending OAS, can be
achieved by prefixing the new property with “x-”.
14
2.5. MICROSERVICES
All APOSTL annotations take advantage of OASs ability to add custom properties.
These annotations are enclosed only within the following properties:
x-invariants can be found in the beginning of an API description and contains a list of
all APIs invariants.
x-requires can be found in the beginning of an operation description and contains a list
of all operations preconditions.
x-ensures can be found in the beginning of an operation description, after the x-requires
property, and contains a list of all operations postconditions.
x-regex can be found either within the description of a models property or in the description of an operation parameter and contains a regular expression that correctly
generates the property or parameter.
15
C h a p t e r
3
Related Work
This chapter presents some black-box testing techniques as well as a comparison between
them. It also features some tools that automatically generate test data in different circumstances. Since the purpose of this thesis is to, ultimately, fully automate the testing
process of microservices, the presented tools are intrinsically related to this subject. A
brief description of HeadREST a more expressive specification language than the ones
currently used in the industry can also be found in this chapter. There are also described
some industrys current practices concerning microservice testing.
3.1 Black-Box Testing Techniques
3.1.1 Random Testing
Random testing is one of the most popular black-box testing methods [8]. Its implementation is not complex and when the systems specification is incomplete it is the only
applicable testing technique.
An operational profile can be obtained through partitioning the input domain and
assigning a probability to each partition. For programs where the operational profile is
known, for whose domain a pseudorandom number generator is available, and for which
there is an effective oracle, the general idea behind random testing follows the steps [10]:
1. Selection of a test case size, N.
2. Assign a probability pi
to each one of the K operationals profile partitions. Each
partition has an unique domain, hence partition i is now mentioned as Di
.
3. Generation of Ni
test cases from the pseudorandom number generator for partition Di such that Ni = piN, for 1 ≤ i ≤ K, i.e., the generator will pick a number
within Di with probability pi
. All these Ni
form the test set.
17
CHAPTER 3. RELATED WORK
4. Execute the program with the generated inputs.
5. Use the oracle function that checks if a result satisfies the systems requirements
to detect any failures. If any failures are detected the software suffers adjustments
and is, once more, tested with a new pseudorandom test set with the same size.
When no failures are detected for a test set with size N, the testing is complete.
For programs where inputs are not straightforward e.g. objects instead of only numbers and strings , partitions are defined for sequences of inputs, i.e., the operational
profile describes “classes of input sequences” [10] and the previously described procedure can be used to randomly select a test set of sequences. The most common case is
random testing being applied with only a requirements document that has no information
about input sequences by the absence of usage information. Thus, it is common that the
operational profile is not available since the input is not made up of single values. When
this happens, random testing is applied with a uniform distribution, i.e., attributing the
same selection probability for every class of input sequences.
3.1.2 Specification-Based Testing
The foundation of every specification-based testing technique are user requirements
generally specified in a formal logical language regarding the softwares functional
behaviour. By having the requirements formally expressed, it is possible to automate
both test case generation and verdict construction. The general steps of specificationbased testing are the following [11]:
1. Test Case Generation:
Generation of a test case i in which the preconditions present in the user requirements are satisfied.
2. Test Case Execution:
Execution of test case i on the system under test produces a result o.
3. Oracle:
Analysis of the pair (i, o) with the requirements through a constraint checker to
determine a verdict about the generated test case i. If the pair satisfies the requirements the test case i passes, otherwise it fails.
3.1.3 Learning-Based Testing
Learning-Based testing emerged with the purpose of improving specification-based blackbox testing. This is achieved by the automatic generation of a vast number of test cases
within a reasonable time frame and, at the same time, improving test case quality by
taking into account the result of previously executed test cases.
18
3.1. BLACK-BOX TESTING TECHNIQUES
In LBT all learning can be classified as active learning [11] since different algorithms
are used to generate new queries (test cases) during the learning process. Three types of
queries can be identified [11]:
Model checking queries generated by model checkers
Structural queries generated by learning algorithms
Random queries generated by random data generators
Test efficiency here defined as the number of queries needed to find an error is
influenced by query type. Therefore, queries should be seen as “expensive”, meaning the
most efficient type of query should be chosen at all times. Empirical evidence shows that
random queries result in the least efficient test cases [11]. Hence, LBT is an improvement
to the pure random testing technique unless the error distribution of the system under
testing is very large , since it finds errors that would be hard to find by using random
testing, in a more time-efficient manner.
The novelty of learning-based testing, against the previously described process of
specification-based testing, is the introduction of a feedback loop [11] into the process previously described, which can be accomplished by introducing a learning algorithm with
the purpose of trying to infer a model of the system based on the already generated test
data, i.e, pairs (i, o). This model is then automatically analysed with the intent of finding
counterexamples in the learned model to the requirements correctness, i.e. to check if
the learned model diverges from the specification. The newly found counterexamples are
then treated as a new test case. If the model is accurate then theres a high probability
that the new test case will incur in an error expected result different from the obtained
result. The accuracy of the model tends to improve over time since it is constantly fed
with new, already executed, test cases.
The choice of a learning algorithm should not be taken lightly since it infers the
models used to generate new test data. Further information regarding suitable learningbased testing algorithms can be found in the following articles by Meinke [12], Meinke
and Sindhu [13].
3.1.4 Adaptive Random Testing
Adaptive Random Testing (ART) was first introduced by Chen et al. [14] and it was
developed to improve the failure-detection effectiveness of random resting. It relies on
“empirical observations showing that many program faults result in failures in contiguous
areas of the input domain” [14]. Hence, one can infer that regions of the input domain
where the software produces results according to the specification, i.e., are correct, are
also contiguous. Therefore, if a set of previously executed test cases have not lead to
failures, the likelihood that test cases farther away from the previously executed ones will
19
CHAPTER 3. RELATED WORK
lead to a failure increase. Therefore, if previous tests have not led to failures, new test
cases should be distant from the already executed ones.
Since the objective of a software tester is to maximize the number of detected faults
and these faults are proven to occur in contiguous regions of the input domain, theres
a need to change the pure random testing technique in some way that introduces some
diversity into the generated test cases, i.e., test cases should be evenly spread through the
input domain.
In order to implement the ART technique, one can follow several approaches. The
even spread of test cases can be achieved from different algorithms following each approach. The most commonly used approaches are the following [8]:
Selection of the best test case from a set of test cases: This technique starts by computing a set of random inputs where the best candidate should be drawn. The most
commonly used algorithm implementing this approach is Fixed Size Candidate Set
ART (FSCS-ART) [15]. Since this was the first algorithm implementing ART and,
according to [8], has been the most cited ART algorithm, it is the one chosen to
illustrate the technique in this document.
Fixed-Size-Candidate-Set Adaptive Random Testing Algorithm
Whenever a new test case has to be chosen, a fixed-size candidate set of random
inputs is generated. For each candidate set a selection criteria is applied to select the
best candidate as the next test case. The selection criteria can be, amongst others,
maxi-min or maxi-sum. It is necessary to compute the distance or some measure
of dissimilarity, for non-numerical inputs between the previously executed test
case and all the candidates. If the selection criteria is maxi-min then the candidate
farther away from the previously executed test case is the chosen one. If the selection criteria is maxi-sum, the distances between each candidate and all the previous
executed test cases are added together being the candidate with the greater sum
value the chosen one.
One of the problems with these algorithms is that a distance or dissimilarity
measure is not naturally defined for non-numerical inputs.
Exclusion: All methods following the Exclusion approach have an exclusion region for
each previously executed test case. Random inputs are generated until one input
is outside all exclusion regions. When an input following this criteria is generated,
it is selected as the next test case to be executed and, consequently, an exclusion
region is defined around it.
Partitioning: The Partitioning approach demands the input domain to be divided into
several partitions. The next partition from where the next test case is generated is
chosen by taking into account the previously executed test cases, i.e., from where
20
3.2. TOOLS FOR AUTOMATED TESTING
they were drawn. Further information on this subject can be found in the article by
Chen et al. [15].
Test Profiles: In this approach, an unique test profile is developed in order to fulfill
the requirement of even spreading of test cases throughout the input domain as
opposed to random testing where the test profile commonly follows an uniform
distribution. More information on test profiles can be found in the article by Liu et
al. [16].
Metric-Driven: This approach has the peculiarity of using distribution metrics, such as
discrepancy or dispersion, as selection criteria to the next test case to be executed.
The usage of metrics as criteria has the purpose of evenly distribute test cases
throughout the input domain.
Further information on different implementations of ART algorithms can be found in
the following documents: Chen et al. [17, 18], Ciupa et al. [19], Lin et al. [20], Mayer [21],
Shahbazi et al. [22] and Tappenden and Miller [23].
3.1.5 Discussion
Although all previously presented techniques can be applied to automatically generate
test data for microservice testing, some are more suitable than others. A pure random
approach is inadvisable, since it can produce redundant and meaningless data.
On the other hand, a learning-based testing technique can be used, since it is able to
find errors typically hard to find with pure random testing. With the proper learning
algorithm, the inferred systems model can be accurate enough for the tester to be able to
affirm that the next generated test case will incur in an error.
Adaptive Random Testing technique, like LBT, is a major improvement to pure random testing. By assuming that faults result in failures in contiguous areas of the input
domain, several approaches were developed to fulfill the requirement of test data being
evenly spread throughout the input domain. Since this idea can incur in an undesirable
overhead, it is necessary to choose the best ART approach as well as the best algorithm
implementing it.
3.2 Tools for Automated Testing
Although these tools do not aim to test microservices directly, the process can be applicable to microservice testing.
3.2.1 QuickCheck
QuickCheck [24] is a tool that generates random test data for Haskell programs. Haskell
is a purely functional programming language which makes programs written in it very
21
CHAPTER 3. RELATED WORK
well suited for automatic testing. This happens because pure functions, i.e., non sideeffecting functions, are easier to test than side-effecting ones. Hence, small code portions
can be tested separately, allowing the software tester to perform meticulous testing at a
small granularity.
The authors state that a testing tool must be able to:
1. Determine whether a test has passed or failed:
The user defines expected properties of the functions under test in a domain-specific
language, designed by the authors.
2. Automatically generate suitable test cases:
The technique used to generate test cases is random testing. Although it may seem
a naive approach, the authors based their choice on results presented by Duran
and Ntafos [25] showing that the difference in effectiveness of random testing and
partition testing is small.
Furthermore, it was a requirement that QuickCheck was a lightweight tool. Using
more systematic methods (e.g. partition testing) would violate this requirement
because some adequacy test criteria [24] needed to be reinterpreted before it could
be applied to functional programs. Not to mention that applying these methods
would require compiler modifications and hence bond QuickCheck to a particular
implementation of Haskell, making their choice of using random testing very clear.
Since random testing is used, it is necessary to discuss the distribution of the test data.
As stated above, the efficiency of random testing is maximized when the distribution of
the test data is the same of the actual data. QuickCheck does not infer a distribution.
Instead, the authors defined a test data generation language, allowing the tester to program
a suitable generator, controlling the distribution of test cases.
3.2.2 JET
JET is an evolutionary testing tool [26] developed with the purpose of automating random testing of Java programs to detect as many inconsistencies as possible between the
specification written in Java Modeling Language (JML) and its implementation. JET
automatically generates test data through a pure random approach , executes the tests
and determines the tests results using a runtime assertion checker as an oracle , thus
fully automating the testing process.
Notwithstanding the utility of the tool by itself, there is an extension to JET, developed
by Cheon and Rubio-Medrano [27], in which test data generation is not purely random.
To randomly construct a Java object without having direct access to its internal state
means the object has to be constructed via method calls. Thus, test data consists of sequences of method calls. Objects methods are divided into three categories: constructors,
mutators and observers. By using a pure random technique, method calls constructors
22
3.2. TOOLS FOR AUTOMATED TESTING
and mutators since observers do not contribute to objects state alteration are randomly
selected, all at once, hence not ensuring the produced object is in a consistent state. A
study shows that more than 50% of randomly generated test data are redundant [27].
Hereupon, the extensions goal is to generate meaningful, not redundant, test data. This
is achieved by constructing the object incrementally i.e. not determining the call sequence at once , ensuring the validity of each randomly selected method call. Hence, an
object is constructed only by feasible method calls verified by JMLs assertion checker
guaranteeing the “randomly” generated object is in a consistent state. In order to solve
the redundancy problem, when generating a new object, a pool of previously generated
(and consistent) objects is used: an object is picked from the pool and then a new call
sequence is appended to it, thus generating a new, consistent and not redundant object.
By using this approach, there is a minimum increase of 10% [27] in the number of
successfully generated test cases.
3.2.3 Korat
Korat is a framework that uses specification-based testing to automate the testing process
of Java programs [28]. Given a methods formal specification written in any specification
language as long as it can be translated to Java predicates , Korat uses the precondition
to generate test cases up to a given size. It then invokes the method on each generated
test case and uses the post-condition as the oracle.
The most interesting aspect of Korat is the technique for test case generation: given
a predicate and a bound on the size of its inputs, Korat generates all non-isomorphic
inputs that verify the predicate, i.e., for which it returns true. In order to generate valid
test cases for a method, Korat creates a class whose fields are the methods parameters,
including the implicit parameter this. This class also has a predicate function returning
a Boolean value , which is, essentially, the methods precondition. It then generates all
distinct inputs for which the predicate returns true. Since the predicate is the methods
precondition, all generated inputs are valid inputs.
To check the correctness of a method, all methods valid inputs are generated. Next,
the method is invoked on each generated input, testing, in each iteration, if the produced
output is correct, using the oracle. If its not, then the input is a counterexample and the
method under test is incorrect [28].
One of the most relevant experimental results using Korat is that theses results prove
the feasibility of automatic test case generation for Java predicates even when the search
space for inputs is very large [28].
3.2.4 Discussion
QuickCheck was developed with the purpose of randomly generating test data for functional programs. It uses a pure random testing strategy and does not even try to infer test
23
CHAPTER 3. RELATED WORK
data distribution. For these reasons, QuickCheck approach is considered to be the least
valuable for the purpose of automatically generate test data in order to test microservices.
On the other hand, the extension to JET does not follow a pure random testing approach: test data is built incrementally and its validity verified in each iteration, leading
to automatically generated, not redundant, test data. This approach can be, with some
adaptations, applied to microservices: constructor methods can be POST actions, mutators can be PUT and DELETE actions and, observers can be GET actions. Hence, this
technique can be used, with a few tweaks, to automatically generate test data for microservice testing.
The main idea behind Korats is that by having both pre and postconditions, being
able to automatically generate test cases based on the precondition only generating valid
test cases and test the methods performance with the postcondition the oracle. This
approach can also be directly applied on microservice testing since pre and postconditions
are assumed to be available. If the postcondition is not available, the oracle can be an
invariant.
In short, both QuickCheck, the JET extension and Korat approaches can be used to
test microservices, being the least preferable the pure random testing technique used by
QuickCheck since it tends to produce an undesirable amount of meaningless data.
3.3 Extending OpenAPI: HeadREST
HeadREST is a language to describe RESTful APIs developed by Vasconcelos et al. as a
part of Confident, a research project on the formal description of RESTful web services
using type technology [1]. HeadREST allows to specify data properties and to observe
server state changes through assertions. These assertions are Hoare triples of the form
{φ} (a t) {ψ}
where a ∈ {GET, POST, PUT, DELETE}, t is an URI e.g., in figure 2.1, /pet/{id}
and both φ (precondition) and ψ (postcondition) are predicates. This assertion should be
interpreted as: if a request to execute action a over the URI t has data satisfying φ and
a is executed on a state satisfying φ, then both the data carried by the response and the
resulting state satisfy ψ [1].
The motivation behind the creation of HeadREST lies on the fact that the current way
of specifying APIs is mainly focused on the structure of the exchanged data and therefore,
ignore the ability to relate different parts of the same data, the relationship between input
and the services state, and, finally, the relationship between input and output. Recalling
the Pet Store example, figure 2.1: supposing a pet has an owner and this owner has a name
and a nickname, there is no way, in the currently available API specification languages
e.g., OpenAPI Specification , to specify that, e.g., the nickname must not have more
than 15 characters. HeadREST is a more expressive way of specifying APIs, relying on
two main ideas [1]:
24
3.4. CURRENT INDUSTRIAL PRACTICES
• Types that allow to express data exchanged in the interactions and properties of
server states
• Pre and postconditions to express the relationship between the input what was
sent in the request and the output what comes in the response.
To make OpenAPI suitable to be used for test case generation, a similar approach to
HeadREST will be used.
3.4 Current Industrial Practices
Industrys most used tools to test microservices are described in this section with the
purpose of illustrating the demand for a method/technique to fully automate the process
of testing microservices.
3.4.1 Manual Testing
None of the following tools can be considered automated testing since test data is produced manually, the microservice is manually invoked once for each test, and the verification is not made by an oracle.
cURL cURL, or client URL [36], is a project providing a library and a command-line tool
to ease data retrieval through several protocols. When the chosen protocol is HTTP,
the user is expected to provide the URL, the headers, and body of the request. In
spite of the ultimate goal of this tool being data retrieval, is has been used to test
microservices manually: the tester makes a request using cURL and then checks
if the response matches the expectations. Needless to say this process is very time
consuming and, therefore, not suitable to testing microservices in a large scale.
Postman Postmans main goal [37] is to design, build and test APIs. However, it can also
be used to test microservices by making requests, just like the previous tool, and
comparing the obtained results with the expected ones. Postman can be used to
manually test a microservice in the same way as cURL, with the only difference
being that Postman provides an easy to use GUI. Postman also organizes requests
in collections allowing the tester to reuse a previously done request.
3.4.2 Semi-Automated Testing
The following tools can be considered semi-automatic since results validation is made
automatically although test data needs to be provided by the tester.
Dredd Dredds main goal is to test APIs implementations. Given the APIs description
document supported languages are API Blueprint and Swagger [38] , Dredd creates expectations based on requests and responses specified in the given document,
25
CHAPTER 3. RELATED WORK
then it requests resources to the API being tested, and verifies if the obtained results
are according to the specification. For operations requiring parameters, Dredd uses
values provided in the specification or, if none is present, Dredd generates some
dummy values according to the provided schema (or data model) e.g. Swaggers
schema is defined in JSON [39]. In spite of Dredd being able to generate test data,
it does not mean the generated data is valuable, i.e., it may not happen on a real
situation. For this reason, Dredd is only a reliable testing tool if test data is provided
by the tester.
Postman Postman eases manual testing, as seen previously, however, it has more interesting features: it also provides a way to kind of automate the testing process by
allowing the tester to write scripts [40], in JavaScript, that are able to validate the
obtained response.
26
C h a p t e r
4
Solution Design
Microservices are commonly used as black-box systems, meaning its consumers are oblivious of its implementation. However, microservices are accompanied with APIs that can
be used as test artifacts. Although these APIs are usually well documented, they lack
essential information for testing purposes. As such, microservices APIs need to be extended in order to accommodate contractual information (described in section 2.3) about
each operation pre and postconditions and about the APIs valid state invariants.
These additional annotations are written in APOSTL, a specification language for describing API invariants and operations pre and postconditions. Microservices APIs also have
information about the data structures exchanged in each operation. Therefore, this data
schema can be improved by including information on how each element can be generated. In short, having a microservice description document with information regarding
the systems state prior and post an operation, and information regarding how a data
structure can be generated provides us with all the information needed to automate the
microservice testing process.
PETIT is an automated microservice testing tool which only requires the microservice
specification properly annotated with APOSTL. This specification language has the particularity that all operations used to describe predicates need to be pure, meaning they
cannot produce any side-effects to the microservices state.
Figure 4.1 illustrates all the steps a user needs to perform in order to use PETIT. As
shown in the figure, the user must first annotate the OAS file with its contract. The
next step is to annotate the same file with the regular expressions, needed for the data
generation. Once the OAS is complete, the user is ready to execute PETIT. Hence, one
must specify the OAS document path and define the order in which operations categories
will be tested. Then, and optionally, one can specify the API testing order random or
sequential, the later meaning “the order as defined in the OAS document” as well as the
27
CHAPTER 4. SOLUTION DESIGN
output form verbose or standard mode. The standard execution only displays the testing
results. If PETIT is executed in verbose mode the response contents of each operation will
be shown. In the verbose mode execution there is also the need to specify the maximum
number of REST resources to be displayed.
Figure 4.1: Steps needed to execute PETIT.
The testing methodology followed by PETIT begins with categorizing all APIs operations into three disjoint sets: mutators composed by PUT and DELETE methods, constructors composed by POST methods, and observers composed by GET methods. This
compartmentalization serves the purpose of manipulating the order in which each category is being tested. The operation order within each category is randomized.
The testing process of each API operation starts by checking if all APIs invariants hold
and, if they do, the testing process proceeds by generating or recycling the needed data,
when applicable. Then, precondition verification begins and, if all conditions hold, the
HTTP request is performed. Once a response is received, the postcondition verification
takes place and the testing process is complete.
Precondition Request Outcome
True 200 OK
True 4XX Failed (analyse execution trace)
False 200 NOT OK
False 4XX Failed (as expected)
Table 4.1: Operation test outcomes.
28
4.1. TOURNAMENTS APPLICATION
The possible test outcomes for a single operation are described in table 4.1. According
to the outcomes presented in the table, when all preconditions hold (true) and the operations response was not successful (4XX) the test failed, and there is the need to analyse
the execution trace, e.g, this scenario usually happens when one is trying to retrieve a
resource that was previously deleted. When the there is at least one precondition that
does not hold (false) and the operations response was not successful (4XX), the test has
failed as expected, since the preconditions did not hold in the first place.
This chapter describes the design process behind both PETIT and APOSTL, as well as
illustrate the fundamental concepts with an example application.
4.1 Tournaments Application
In order to better understand how to use PETIT, consider a tournaments application
composed by two APIs players and tournaments API. This applications purpose is
to manage players enrollments in different tournaments. As such, a player can be both
enrolled and disenrolled from a tournament, as long as the number of enrolled players has
not reached the tournaments capacity. Figures 4.4 and 4.5, respectively, depict players
and tournaments APIs.
The players API manages all player resources which are identified by the playerNIF
property, and composed by the properties shown in figure 4.2. The property tournaments
is a collection of the tournaments in which the player is enrolled. When expanded, it
shows the tournaments schema, depicted in figure 4.3.
Figure 4.2: Player schema from tournaments application.
On the other hand, tournaments API manages all tournament resources which are
identified by the tournamentId property and composed by the properties shown in figure 4.3. The property players is a collection of the players enrolled in the tournament.
When expanded, it shows the players schema, depicted in figure 4.2.
As seen in figure 4.4, players API describes all operations responsible for managing a
player resource. These operations are responsible for inserting, updating, retrieving and
deleting a player from the system as well as retrieving a players enrollments.
29
CHAPTER 4. SOLUTION DESIGN
Figure 4.3: Tournament schema from tournaments application.
Figure 4.4: Players API operations.
Similarly, the tournaments API, as seen in figure 4.5, describes operations responsible
for managing a tournament resource and, as such, one can insert, update, retrieve, and
delete a tournament, retrieve a tournaments capacity and its enrollments, as well as both
enroll and disenroll a player from a tournament. Both APIs have operations to retrieve
all their managed resources.
The tournaments application is the case study used throughout this thesis and, as
such, it will be frequently referenced in future chapters, serving as a base to explain the
fundamental concepts both for the conditions written in APOSTL as well as the testing
methodology implemented by PETIT.
4.2 Specification Language: APOSTL
APOSTL is a specification language to annotate APIs specifications based on first-order
logic. It has the purpose of extending the currently used API specification languages with
properties that can be useful for testing purposes, transforming these documents into
useful testing artifacts. Besides providing information needed for testing an application,
APOSTL also provides an API with semantic, i.e., with these annotations one can easily
understand each operations logic.
APOSTLs main feature is the ability of writing logical conditions based on pure (without side-effects) API operations. These conditions are used to write operation contracts.
30
4.2. SPECIFICATION LANGUAGE: APOSTL
Figure 4.5: Tournaments API operations.
In the same way, APOSTL is also used to write API invariants. Although being initially designed for extending OAS, APOSTL can also be used with any API specification language
that has the ability to be extended.
While developing APOSTL, there was a concern that was always present: usability.
The problem with many specification languages is that in order to use them effectively,
one needs to conquer a challenging learning curve. With APOSTL, the specification
developer will only need to know a few intuitive keywords, basic knowledge of first order
logic and its own API.
Considering the proposed example the tournaments application and focusing on
the operation responsible for inserting a player from players API, one can derive some
logical properties that should constitute this operations contract:
Precondition Only a player that does not exist can be inserted.
Postcondition After the insertion, the player must be in the system.
This contract states that if the client follows the precondition then the server will
ensure the postcondition is held. In APOSTL, these two conditions should be written
only at the cost of pure operations which, in RESTful APIs, translates into GET operations.
As such, one way of writing the contract for this operation is depicted in listing 4.1.
31
CHAPTER 4. SOLUTION DESIGN
// Precondition
response_code(GET /players /{ playerNIF }) == 404
// Postcondition
response_code(GET /players /{ playerNIF }) == 200
response_body(this) == request_body(this)
Listing 4.1: Players API POST player operation contract.
APOSTL takes advantage of the standardized HTTP codes. As seen in listing 4.1, the
precondition states the response code of a request to get the player yet to be inserted must
return the code 404 (resource not found). Similarly, the postcondition states that after
the insertion, the same request should return the response code 200 (OK), meaning the
player is persisted in the system. The second postcondition might not be as trivial as the
previous one: the response body of the POST request must be equal to the same requests
body. This condition ensures that what is returned form the server is exactly what was
sent by the client.
With APOSTL one can also access the previous state of an API. The operation responsible for deleting a player makes use of this feature. This operations contract is described
in listing 4.2.
// Precondition
response_code(GET /players /{ playerNIF }) == 200
// Postcondition
response_code(GET /players /{ playerNIF }) == 404
response_body(this) == previous(response_body(GET /players /{ playerNIF }))
Listing 4.2: Players API DELETE player operation contract.
The precondition states that for a player to be deleted it must exist. The first postcondition states that, if the precondition holds, then the player is deleted from the system.
The last postcondition, once again, is regarding the contents of the servers response: the
response body must be equal to the response body from a request retrieving the same
player before the current request is performed, i.e. the deletion.
APOSTL also allows the usage of quantifiers. For instance, one invariant for the tournaments API is depicted in listing 4.3.
// Invariant
for t in response_body(GET /tournaments) :-
response_body(GET /tournaments /{t.tournamentId }/ enrollments ). length <=
response_body(GET /tournaments /{t.tournamentId }/ capacity)
Listing 4.3: Tournaments API invariant.
32
4.3. TESTING TOOL: PETIT
This invariant states that, for all tournament resources, the number of the tournaments enrolled players needs to be less or equal to the tournaments capacity.
4.2.1 Data Generation
Once all API operations are properly annotated with invariants, pre and postconditions,
one can also provide information on how to generate exchanged data. This information
is specified using regular expressions. Returning to the previous example the tournaments application , and considering the operation responsible for retrieving a single
player, partially specified in 6.8. This operation has a potentially interesting parameter,
of the type string, playerNIF. The parameter schema of a regular OAS would normally
just have the property type. However, an additional property was added, x-regex. If this
property is present, PETIT will generate data according to the information described in
the regular expression.
1 "/players/{playerNIF}":
2 get:
3 summary: Return a player by NIF .
4 xr e q u i r e s :
5 - T
6 xensures :
7 - T
8 parameters:
9 - name: playerNIF
10 required: true
11 schema:
12 type: s t r i n g
13 xregex: "(1|2)[0 -9]{8}"
Listing 4.4: YAML object for Players API get player operation.
As previously mention, APOSTL is based on first-order logic with some restrictions.
The restrictions are mainly focused on nested conditions, e.g., APOSTL does not allow
nested quantifiers nor quantifiers with more than one variable. Restrictions will be further discussed in the implementation chapter.
4.3 Testing Tool: PETIT
This thesis proposes a new methodology for automatically testing microservices, having
only access to its API description file. The developed tool, PETIT, is able to test microservices when provided with an OAS document, written in JSON and properly annotated
with the previously proposed specification language, APOSTL.
PETIT is made up of several components, each one being responsible for a different
stage of the testing process. Its architecture, depicted in figure 4.6, shows not only the
33
CHAPTER 4. SOLUTION DESIGN
different components of PETIT, but also its execution flow, from the point where the
specification file is provided to the API testing results.
As seen in figure 4.6, the OAS file is processed by the specification parser component,
which is responsible for taking the information of the API description and make it available as Java objects. Thus, the specification parser produces a specification object and
several schema objects. The schemas are used by the input generator component in order
to only generate valid test data, i.e., valid JSON elements. The specification, in turn,
is used by the formula parser which is responsible for not only replace the parameters
with the generated test data, but also to analyse if the resulting formula is according to
APOSTL. Finally, the tester and evaluator will, as the name implies, be responsible for
testing the application and evaluating the results. As such, it verifies the invariants and
preconditions and forwards the requests to the HTTP manager component, which has the
purpose of performing all needed requests to the microservice, process and forward the
received responses to the tester and evaluator. The tester and evaluator then evaluates the
preconditions and invariants and outputs the API testing results.
Figure 4.6: PETITs architecture.
As previously mentioned, PETIT can be executed with the following four parameters,
only two of them being mandatory:
34
4.3. TESTING TOOL: PETIT
File Path the complete path to the JSON file containing the OAS document.
Operation Order Strategy APIs operations are categorized into Constructors, Mutators
and Observers. The order strategy is the order in which these operations categories
will be tested. The operation order within each category is random. Hereupon, a
valid strategy would be, e.g., CMO where the constructors would be tested first,
then the mutators and, finally, the observers. Operations can also be tested randomly by providing RND as the strategy. When this parameter is wrongly specified
the message in listing 4.5 is displayed.
Invalid operation order strategy.
A valid strategy is composed of three characters meaning the following:
> C: constructors (POST)
> M: Mutators (PUT , DELETE)
> O: Observers (GET)
> RND (random)
A valid strategy would be, e.g., CMO
Listing 4.5: Error message when operation order strategy is wrongly specified.
Verbose Mode (-v) if this flag is present, all performed requests responses will be shown.
This mode is accompanied by another argument which indicates the number of
resources to be printed.
Random API Order (-r) if this flag is present, the APIs described in the specification
will be shuffled and tested in a random order.
Both the file path and operation order strategy parameters are required. The remaining are not required and, therefore, the order in which they are specified is irrelevant.
PETITs output is a detailed description of the testing process results. It comprises
detailed information on what is happening during each stage of the testing process, while
testing each operation. When an API test is complete the number of succeeded, failed,
and inconclusive tests are shown. Since PETIT is making changes to the microservices
database it also reverts all changes when the test process is finished. This cleanup is
particularly important since PETIT only generates valid input data and, if not removed,
besides wasting memory, it may cause, e.g., a tournament to be full when, in fact, it is
full with dummy players. Listing 4.6 shows PETITs output when testing an API with a
single operation.
35
CHAPTER 4. SOLUTION DESIGN
>>> Testing POST /players
> Verifying Invariants : OK
> Generating Data : OK
> Verifying Preconditions : OK
> Performing Request : OK
> Verifying Postconditions : OK
--------------------------------------------------------
POST /players : OK
----------------------------------------------------------
>>> Player s API Results:
OK : 1
NOT OK : 0
INCONCLUSIVE : 0
>>> REVERTING ALL EFFECTS : OK
Listing 4.6: PETITs output when testing an API with a single operation.
With all this information in mind, one possible way of executing PETIT is depicted
in listing 4.7. This would execute PETIT in verbose mode (showing a maximum of two
resources), with random API order and MCO (mutators, constructors and observers) strategy.
$ java -jar PETIT.jar openapi.json CMO -v -r
>>> Maximum resources to be printed: 2
Listing 4.7: PETITs output when testing an API with a single operation.
This chapter provided the core concepts to understand both APOSTLs and PETITs
design process. The next chapters will present an implementation as well as its limitations.
36
C h a p t e r
5
Solution Implementation
This chapter presents essential information on how PETIT and APOSTL are implemented.
The specification language implementation section illustrates how the Open API Specification extension and how APOSTLs integration with PETIT were achieved, as well as a
formal definition for APOSTLs grammar and its restrictions.
The testing tool implementation section describes the most relevant aspects of PETITs
implementation, namely a detailed description of all its architectural components, the
testing process it implements, and the detailed process for valid test data generation.
5.1 Specification Language: APOSTL
As previously mentioned, APOSTL is a specification to annotate APIs specifications with
useful contracts for testing purposes, based on first-order logic with some restrictions.
This section aims to expose the needed steps to implement APOSTL, namely how the
extension of Open API Specification is achieved, a formal description of APOSTLs rules,
and APOSTLs restrictions.
5.1.1 Extending OpenAPI Specification
Open API Specification allows the addition of custom properties to a specification description. In order to accommodate APOSTLs conditions in an OAS document, there
were added three new properties: x-requires for the preconditions, x-ensures for the postconditions, and x-invariants for the invariants. It was also added a fourth property to
aid in custom test data generation, x-regex. This last property can be found in schemas
descriptions such as in operations parameters schemas and model schemas.
The properties representing operations contracts x-requires and x-ensures , and the
property representing API invariants x-invariants are collections, meaning they can
37
CHAPTER 5. SOLUTION IMPLEMENTATION
have more than one APOSTL condition. On the other hand, x-regex property can only
comprise a single regular expression.
As seen in section 2.5.3, the OAS document has a well defined structure. Although
custom properties can be added anywhere in the document, their position could interfere in readability and usability. As such, the main concern was where should the new
properties be added so that its position is not disturbing and is easy to understand to
which operation, or API, do they belong to. Returning to the tournaments application
description, listing 5.1 depicts the partial description of the operation responsible for
player deletion. As seen in the listing, x-requires and x-ensures, concerning operations,
appear in the beginning of an operation description, right after its summary. When the
operation has a parameter, the information concerning the parameter generation, x-regex,
appears within the parameter schema description, also depicted in listing 5.1.
1 "/players/{playerNIF}":
2 d e l e t e :
3 summary: Delete the player with the given NIF .
4 xr e q u i r e s :
5 - response_code (GET / players / { playerNIF } ) == 200
6 xensures :
7 - response_code (GET / players / { playerNIF } ) == 404
8 - response_body ( t h i s ) ==
9 previous ( response_body (GET / players / { playerNIF } ) )
10 parameters:
11 - name: playerNIF
12 schema:
13 type: s t r i n g
14 xregex: "(1|2)[0 -9]{8}"
Listing 5.1: YAML object for Players API delete player operation.
Invariants are conditions concerning APIs and, as such, they appear in the beginning
of APIs descriptions. Listing 5.2 shows the beginning of the tournaments API description and where the its x-invariants property is located.
1 "/tournaments":
2 xi n v a r i a n t s :
3 - f or t in response_body (GET / tournaments ) :
4 response_body (GET / tournaments / { t . tournamentId } / enrollments ) . length
5 <= response_body (GET / tournaments / { t . tournamentId } / capacity )
Listing 5.2: YAML object for Tournaments API.
With this implementation every new property is as close as possible to what relates
to without, at the same time, being too intrusive hampering usability.
38
5.1. SPECIFICATION LANGUAGE: APOSTL
formula ::= quantifiedFormula | booleanExpression
quantifiedFormula ::= quantifier string in call :- booleanExpression
quantifier ::= for | exists
call ::= operation | operationPrevious
booleanExpression ::= booleanExpression booleanOperator booleanExpression | clause
clause ::= T | F | comparison
comparison ::= term comparator term
term ::= operation | operationPrevious | param
operationPrevious ::= previous ( operation )
operation ::= operationHeader ( operationParameter ) function?
operationHeader ::= request_body | response_body | response_code
operationParameter ::= httpRequest | this
httpRequest ::= method | url
url ::= segment+
method ::= GET | POST | PUT | DELETE
comparator ::= == | != | <= | >= | < | >
booleanOperator ::= && | || | =>
param ::= string (. string)* | int
segment ::= / block(. block)*
block ::= { blockParameter } | string
blockParameter ::= string (. string)? | operation | operationPrevious
function ::= . string
Table 5.1: APOSTLs grammar defined in BNF.
5.1.2 Grammar
APOSTLs grammar is a context-free grammar, meaning its non-terminal rules can be
applied regardless of the context it is inserted, meaning the left hand side of a nonterminal rule can always be replaced by the right side of the same rule, independently of
the circumstances where this rule appears.
Backus-Naur form (BNF) is a commonly used notation for describing grammars. Every
rule in BNF has the following structure:
rule_name ::= expansion
An expansion may contain terminal and non-terminal rules. These rules are connected
either by alternatives or sequences. APOSTLs grammar is described in table 5.1. Terminal
symbols are depicted in blue for readability purposes.
An APOSTL formula can either be a boolean expression or a quantified formula. An
example of an APOSTL quantified formula can be found in tournaments API invariant,
as seen in listing 5.2. A boolean expression is recursively defined as being two boolean
expressions, separated by a boolean operator, or a clause. In turn, a clause can either be a
39
CHAPTER 5. SOLUTION IMPLEMENTATION
boolean value true (T) or false (F) , or a comparison, which is made up of two terms,
that can either be APOSTL operations or parameters, and a comparator. An example of
an APOSTL comparison can be found in listing 5.1, which shows a players API operation
contract.
5.1.3 Integration with PETIT
In order for PETIT to be able to evaluate APOSTLs formulas, there is the need to tell
whether a formula is formed according to APOSTLs rules, i.e., its grammar. Hereupon,
there is the need to implement a parser, a program that analyses a sequence of tokens
and checks if this sequence is conforming to the grammar.
Instead of implementing a parser from scratch, PETIT uses a tool to generate it.
ANTLR ANother Tool for Language Recognition is a parser generator that, given a
formal language description, can automatically build and traverse parse trees [29]. Parse
trees are data structures that can be traversed in order to tell whether the input matches
the grammar. A parse tree resulting from running the parser generated by ANTLR with
the formula response_code(GET /players/{playerNIF}) == 404 is depicted in figure 5.1.
Figure 5.1: Parse tree of a conforming APOSTL formula.
When a formula is not conforming to the grammar rules, ANTLR throws an exception
which is, in turn, caught and handled by PETIT.
Integration of APOSTL with PETIT involves not only traversing the parsing tree and
checking formulas conformity to the grammar, but also evaluating APOSTLs formulas
40
5.1. SPECIFICATION LANGUAGE: APOSTL
with the generated input. This will be further analysed in the following section, namely
when describing PETITs component formula parser.
5.1.4 Restrictions
By analysing APOSTLs grammar, described in table 5.1, and as previously referred,
APOSTL does not support nested quantifiers, as depicted in listing 5.3, neither quantifiers with more than one variable, as depicted in listing 5.4.
for t in response_body(GET /tournaments) :-
for p in response_body(GET /tournaments /{t.tournamentId }/ players) :-
response_code (/ tournaments /{ tournamentId }/ enrollments /{p.playerNIF} == 200
Listing 5.3: A nested quantifier, written in APOSTL.
for t in response_body(GET /tournaments),
p in response_body(GET /tournaments /{t.tournamentId }/ players) :-
response_code (/ tournaments /{ tournamentId }/ enrollments /{p.playerNIF} == 200
Listing 5.4: A quantifier with more than one variable, written in APOSTL.
Both these conditions mean the exact same: for every tournament if a player is stored
in the tournaments players collection, the player must be enrolled in the tournament.
There are some restrictions in APOSTLs implementation which, by only analysing
its grammar, could be considered allowed. According to the grammars rules an HTTP
operation can be a GET, POST, PUT or DELETE. However, and as previously referred,
APOSTLs formulas can only be made up of pure HTTP operations, meaning only GET
operations can be used. It is also not allowed for the keyword this to appear anywhere
else but in comparisons. In other words, this cannot appear in a quantified formulas call.
Also contrary to what is described in the grammar, composed block parameters can only
have depth one, meaning that block parameters such the one depicted in listing 5.5 cannot
occur, since it has depth two (p.playerNIF.tournaments).
for p in request_body(GET /players) :-
response_code(GET /players /{p.playerNIF.tournaments }) == 200
Listing 5.5: An invalid block parameter in an APOSTLs formula, according to its implementation.
Although APOSTLs grammar does not have any information about x-regex parameters,
its implementation assumes that schemas cannot have a composed identifier, meaning
each resource can only have one property as its ID. This happens for no particular reason
other than lack of time.
APOSTLs implementation also assumes that properties that serve as IDs cannot have
the same name in different resources. In short, different properties belonging to different
41
CHAPTER 5. SOLUTION IMPLEMENTATION
resources must have different names. This happens to prevent having to specify the
resource type in order to get its ID, i.e., if both players and tournaments resources would
have its identification property named id, there would be the need to refer to them as
t.id and p.id instead of just tournamentId and playerNIF and, consequently, having to
define p as a player and t as a tournament in APOSTL specifications.
5.2 Testing Tool: PETIT
PETIT is a tool which automates the microservice testing process based on its API description. This section aims to illustrate PETITs implementation from its architectural
components to the implemented testing process.
5.2.1 Architecture Components
PETITs overall architecture is shown in figure 4.6. It illustrates all PETITs components
specification parser, input generator, formula parser, tester and evaluator, and the HTTP manager as well as their interactions. All these components are responsible for performing
a different, but equally, important task. As such, their implementation and interactions
will be further analysed.
Specification Parser as the name implies, this component is a parser responsible for
analysing and translating the OAS document. From a JSON specification, it generates a Java object with all the information in the OAS file, and several Java objects,
one for each schema.
Input Generator is responsible for all test data generation. The generator operation, depicted in figure 5.2, begins by checking the operation type POST, PUT, GET or
DELETE. If the operation is a POST or a PUT, it generates a JSON object form the
operations body schema, depicted in figure 5.3. Otherwise, i.e., if it is a GET or a
DELETE and the operation has parameters, the JSON object is generated form the
URL parameter description, depicted in figure 5.4.
Generate form body schema operation, illustrated in figure 5.3, starts by going through
all operations properties. For each property type there is a different outcome. If the
property is a string and, simultaneously, a database generated property then there
is no need to generate it. A flag indicated the property is generated is added to the
object being generated. If the property is a string that is not database generated,
then if it has a regular expression, the string will be generated according to the
regular expression; otherwise a random string is generated. If the property is an
integer and is database generated, the process is the same as described for string
properties. If it is not database generated and it has a minimum value, the integer
will be generated according to that minimum value, ranging from the minimum
42
5.2. TESTING TOOL: PETIT
up until the maximum integer. If the minimum value is not present, then a random positive integer is generated. For properties of the type array an empty one is
generated. For object properties, the generate from body schema operation is called
recursively.
Generate from URL parameter operation, illustrated in figure 5.4, begins by checking if the parameter type is string or integer. In the case of being a string, then
the parameter is generated from the regular expression. Otherwise, the integer is
generated ranging from the specified minimum to the maximum integer.
Figure 5.2: Generate operation logic.
Formula Parser component is responsible for traversing the parsing tree that is generated by ANTLR. Each node of the parsing tree needs to be checked in order to
ascertain if a formula is conforming to the grammars rules. The Visitor Oriented
Parser was developed for that purpose, based on [41]. The visitor design pattern has
the purpose of separating an algorithm from the object it operates on. It allows to
add new functionality to an already implemented class without changing its implementation. A visitor usually operates in a class that is composed by several other
element classes. In APOSTLs case, the formula class is composed by several element
classes such as boolean expression, quantified formula, and so forth.
HTTP Manager as the name implies, it is responsible for the HTTP request and response
management. HTTP responses are parsed into Java objects so they can be easily
manipulated.
Tester and Evaluator has the purpose of implementing the testing process, described in
subsection 5.2.2, managing the generated objects pool, and evaluating all APOSTL
formulas. The object pool is a mechanism implemented in order to enhance PETITs
performance. Every time new test data is generated it is added to the pool. When
data of the same type is needed for another test, instead of generating new data, the
pool is checked and, if there is conforming data, it gets recycled.
An evaluation consists of ascertain the truth value of an APOSTL formula. Algorithm 1 depicts how a quantified formula is evaluated. It starts by retrieving the
43
CHAPTER 5. SOLUTION IMPLEMENTATION
Figure 5.3: Generate body schema operation logic.
Figure 5.4: Generate URL parameter operation logic.
quantified formulas collection from the database. For each element in the collection, the boolean expressions URL parameters are replaced for the elements values.
Then, the resulting boolean expression is evaluated, and its result is stored. If the
formula has the universal quantifier, for the first element that this evaluation result
is false, the quantified formula also evaluates to false. Otherwise, if the formula is
44
5.2. TESTING TOOL: PETIT
quantified by the existential quantifier, for the first element that the partial evaluation is true, the quantified formula also evaluates to true.
Algorithm 1 Evaluation of ALPOSTL quantified formulas.
▷ Evaluates a quantified formula.
1: function evaluateQuantified(parser, formula)
2: isUniversal ← formula.isUniversal()
3: booleanExpression ← formula.getExpression()
4: collectionURL ← formula.getCollectionUrl()
5: collection ← HTTPManager.GET(collectionURL) ▷ perform GET request
6: for elem ∈ collection do
7: parameters ← getConditionURLParameters(booleanExpression)
8: for p ∈ parameters do
9: booleanExpression ← replaceURLParameters(booleanExpression, p, elem)
10: f ← parser.parse(formula) ▷ transform string into formula obj
11: partialResult ← evaluateFormula(f) ▷ evaluate the current expression
12: if isUniversal then ▷ for the first elem that eval is false return false
13: if !partialResult.getValue() then
14: return false
15: else ▷ for the first elem that eval is true return true
16: if partialResult.getValue() then
17: return true
5.2.2 Testing Process
The testing process implemented by PETIT has three core operations, decreasing in granularity: testSpec, testAPI and testOperation.
The testSpec implementation is depicted in algorithm 2. It starts by checking if the
user provided the r flag which, if it is present, means the APIs testing order will be
randomized. After this check, the operation enters a loop testing all APIs, either in the
randomized order or the original order in which they are defined in the OAS file. When
all APIs are tested, all the changes made to the microservice database are reverted by
gathering all operations responsible for resource deletion and performing them on every
object in the object pool, which concludes the specification testing process.
The testAPI implementation is depicted in algorithm 2. The process starts by reorganizing all APIs operations into the order that was specified by the user e.g. CMO
(constructors, then mutators and, finally, observers). Similarly to the previous operation,
it enters a loop verifying the APIs invariants and testing all operations, by the previously
defined order. When all operations are tested, the API testing results are shown and the
API testing process is complete.
Finally, testOperation, depicted in algorithm 2, is responsible for testing each individual operation. This testing step can be divided into two sections: the test data generation
logic and the operation testing per se.
45
CHAPTER 5. SOLUTION IMPLEMENTATION
Algorithm 2 Algorithm for testing a specification and its main functions.
▷ Tests a specification.
1: function testSpecification(spec)
2: APIs ← spec.getAPIs()
3: apiResults ← ∅
4: for api ∈ APIs do
5: apiResults ← testAPI(api)
6: printAPIResults(apiResults)
7: deleteEffects(spec.getDeletes())
▷ Tests a single API.
8: function testAPI(api, strategy)
9: operations ← reorganize(api.getOperations(), strategy)
10: apiResults ← ∅
11: for op ∈ operations do
12: satisfiesInvariants(api)
13: apiResults.add(testOperation(op))
14: return apiResults
▷ Tests an API operation.
15: function testOperation(op)
16: verb ← op.getVerb()
17: url ← op.getUrl()
18: params = getURLParameters(url)
19: if verb , POST then
20: generated ← recycle(params)
21: if generated = null then
22: generated ← generate(op)
23: else
24: generated ← generate(op)
25: addToPools(op)
26: url ← replaceParameters(params)
27: satisfiesPre ← processPreconditions(op, generated, generatedURLParam)
28: previousResults ← processPrevious(op, generatedURLParam, generated)
29: response ← performRequest(op, url, generated) ▷ operations request
30: if verbose then ▷ executed in verbose mode
31: printResponse(response)
32: if res.getCode() , 200 then
33: printCausedBy(response)
34: else
35: satisfiesPos ← processPostconditions(op, generated, response)
36: satisfiesPrev ← satisfiesPrevious(op, generated, response)
37: opOk ← response.getCode() = 200 ∧ satisfiesPre ∧ satisfiesPos ∧ satisfiesPrev
38: failedAsExpected ← res.getCode() , 200 ∧ ¬satisfiesPre
39: analyse ← res.getCode() , 200 ∧ satisfiesPre
40: result ← getOperationResult(opOk, failedAsExpected, analyse)
41: printOperationResult(op, opOk, failedAsExpected, analyse)
42: return result
46
5.2. TESTING TOOL: PETIT
The test data portion starts by checking if the operation is a constructor, i.e. a POST.
If it is, new test data is generated. Otherwise, the generated objects pool is checked. If it
is empty, then new test data is generated. If it has some previously generated elements
and there is at least one element which has the same schema as the element needed to
perform the operation, then this element is recycled, meaning it will be used again for this
operations test. If there is no element with the same schema, a new element is generated.
When the testing data is set, either by recycling or generation, there is the need to replace the URL parameters including the operation URL and all pre and postconditions
with the correct values taken from the elements properties. The replacement operation
implementation is described in algorithm 3. When every parameter is replaced by the
correct values the testing process begins. It starts by verifying if the generated element is
conforming to the preconditions, depicted in algorithm 3. If not, the failed preconditions
are displayed and the testing process is resumed, in order to check the microservices
response. Otherwise, it will search for postconditions with the previous keyword and, if
there are some, they are processed, meaning all its requests are performed; if not, the
testing process continues by performing the operations request. In case the user executed PETIT in verbose mode v flag is present , then the requests response will be
displayed. If the request failed, all the known reasons why it failed are displayed, the
operation testing results are also displayed and the testing process ends. Otherwise, i.e,
if the request does not fail, the operations postconditions are verified depicted in algorithm 3 taking the response and the generated data into account. If a postcondition
fails it is displayed. Postconditions with the previous keyword are now verified taking
into account their results were obtained before the operation request was performed. If
there are some failed postconditions with the previous keyword, they also get displayed.
The operation testing results are displayed and the operation testing process is complete.
This chapter described both PETITs and APOSTLs implementation. The next chapter
aims to point some additional aspects by using PETIT with two different applications: a
correct, and a faulty one.
47
CHAPTER 5. SOLUTION IMPLEMENTATION
Algorithm 3 Auxiliary operations: evaluating contracts and replacing parameters.
▷ Evaluates preconditions and processes its output.
1: function processPreconditions(op, generated, generatedURLParam)
2: failedPreconditions ← satisfiesPRE(op, generated, generatedUrlParam)
3: satisfiesPre ← failedPreconditions = ∅ ? true : false
4: if !satisfiesPrev then
5: printFailedConditions(failedPreconditions)
6: return satisfiesPre
▷ Evaluates postconditions and processes its output.
7: function processPostconditions(op, generated, response)
8: ensures ← removePrevious(op.getEnsures())
9: failedPostconditions ← satisfiesPOS(ensures, generated, response)
10: satisfiesPos ← failedPostconditions = ∅ ? true : false
11: if !satisfiesPos then
12: printFailedConditions(failedPostconditions)
13: return satisfiesPos
▷ Evaluates postconditions with the previous keyword and processes its output.
14: function satisfiesPrevious(op, generated, response)
15: if previousResults , ∅ then
16: failedPrevious ← evaluatePrevious(previousResults, response)
17: satisfiesPrev ← failedPrevious = ∅ ? true : false
18: if !satisfiesPrev then
19: printFailedConditions(failedPrevious)
20: return satisfiesPrev
▷ Replaces URL parameters for generated values.
21: function replaceParameters(parameters, url)
22: if parameters , ∅ then
23: for param ∈ parameters do
24: poolElem ← findObject(param) ▷ checks if the pool has usable obj.
25: if poolElem , null then
26: url ← replaceURLParameters(url, param, poolElem.get(param))
27: else ▷ generate parameter from regex or min
28: regex ← spec.getParameterRegex(param)
29: min ← spec.getParameterMin(param)
30: type ← spec.getParamType(param)
31: generatedURLParam ← generateURLParam(type, min, regex)
32: url ← replaceURLParameters(url, param, generatedURLParam)
33: return url
48
C h a p t e r
6
Evaluation
As previously discussed, PETIT can be executed with different operation order strategies.
Different strategies can lead to different test outcomes. Hereupon, this chapter features
several tests conducted on tournaments application, described in section 4.1, to ascertain
how the order strategy parameter influences the test result. Each of the following sections
illustrate how the different operation categories constructors, observers and mutators
can be tested both for success and failure cases. Recalling the applications description,
one knows that it is made up of two different APIs the players and the tournaments
API. PETIT sequentially tests each APIs operations in the specified order. PETIT is not
executed in random mode r flag , so players API is always tested first. For readability
purposes, this chapters listings only depict non-trivial or error cases, and the order in
which each operation appears is the order in which it is tested.
This chapter analyses PETITs tests results when testing a correct implementation
of the tournaments application as well as a faulty one. Implementation errors will be
incrementally added in order to ascertain if PETIT finds them and, if it does, how useful
is its output.
6.1 Testing Constructors
The most adequate order strategies to test constructor operations for their success case
the used test data is conforming to the constructors contract are COM and CMO. Both
this strategies test constructors first, meaning the following operations being tested use
the resources created by the constructors. If constructors have some implementation error,
it will likely be caught in the following tests. Assuming constructors are implemented
according to its specification, both this strategies can also be used to test mutators and
observers for the success case. On the other hand, if one assumes constructors are not
49
CHAPTER 6. EVALUATION
implemented according to its specification, both observers and mutators will be tested
for their failure scenarios.
Listing 6.1 shows the specification testing results when testing it with COM order
strategy. Although everything appears to be correct, there is always the need to check the
execution trace, i.e, each operations testing output.
>>> Player s API Results:
OK : 6
NOT OK : 0
INCONCLUSIVE : 0
--------------------------------------------------------------------------
>>> Tournament s API Results:
OK : 10
NOT OK : 0
INCONCLUSIVE : 0
Listing 6.1: Specification test results when executing PETIT with COM order strategy.
Listing 6.2 shows PETITs output, when performing the same test, at operation level.
One can see that, besides producing a result that is still considered correct, there were
three operations that were not tested for the success case: inserting, retrieving and removing an enrollment. In listing 6.2 the result of inserting a new enrollment is classified
as failed (as expected). This happens because some preconditions did not hold before
the request was made. Considering the first operation in the same listing inserting a
new enrollment one can see that the operation failed because neither the player nor
the tournament exist in the system and, therefore, a new enrollment could not be added.
Since players API was tested first, there should be, at least, one player stored in the pool.
Recalling the testing process, described in section 5.2.2, one knows that every correctly
generated object is stored in the data pool. The player is, in fact, stored in the data pool
and recycled to test the enrollment insertion operation. However, the players API was
tested first, meaning the player deletion operation was previously tested as well. Therefore, although being stored in the data pool, if the player deletion operation is correctly
implemented the player will not be stored in the microservices database.
The result of the operation responsible for retrieving an enrollment is also labeled
as failed (as expected). This time, the only failing precondition is the one concerning the
player, for the reason previously described. Since the strategy chosen is COM, there is
already a tournament in the system that was not yet deleted constructors are tested
before mutators.
The last operation failing, as expected, is the enrollment deletion. This is the last API
operation being tested and, as such, the failing preconditions concern both the player and
the tournament that were already deleted, and the enrollment that ended up not being
created in the first place.
This test case shows that, even though PETIT labels the specification test as being
successful, not all possible operations outcomes are, in fact, being tested. Hereupon,
50
6.1. TESTING CONSTRUCTORS
there is the need to test the same application with different strategies in order to increase
test coverage. However, since the system under test is a black box, test coverage cannot
be effectively measured in the sense of lines of code or conditional branches covered. In
a black box testing scenario the applications end-user play a large role of determining
the test coverage and, therefore, cannot be measured accurately.
>> POST /tournaments /{ tournamentId }/ enrollments
> Verifying Invariants : OK
> Generating Data : OK
> Verifying Preconditions : NOT OK
> Failed:
- response_code(GET /tournaments /31) == 200
- response_code(GET /players /223893138) == 200
> Performing Request : FAILED (as expected)
> Caused by:
> Code: 404
> Message: Player with NIF 223893138 not found.
--------------------------------------------------------------------------
POST /tournaments /{ tournamentId }/ enrollments : OK
>> GET /tournaments /{ tournamentId }/ enrollments /{ playerNIF}
> Verifying Invariants : OK
> Recycling Data : OK
> Verifying Preconditions : NOT OK
> Failed:
- response_code(GET /players /223893138) == 200
> Performing Request : FAILED (as expected)
> Caused by:
> Code: 404
> Message: Player with NIF 223893138 does not exist.
--------------------------------------------------------------------------
GET /tournaments /{ tournamentId }/ enrollments /{ playerNIF} : OK
>> DELETE /tournaments /{ tournamentId }/ enrollments /{ playerNIF}
> Verifying Invariants : OK
> Recycling Data : OK
> Verifying Preconditions : NOT OK
> Failed:
- response_code(GET /tournaments /2) == 200
- response_code(GET /players /223893138) == 200
- response_code(GET /tournaments /2/ enrollments /223893138) == 200
> Performing Request : FAILED (as expected)
> Caused by:
> Code: 404
> Message: Player with NIF 223893138 does not exist.
--------------------------------------------------------------------------
DELETE /tournaments /{ tournamentId }/ enrollments /{ playerNIF} : OK
Listing 6.2: PETITs partial output of a tournaments API test executed with COM strategy.
51
CHAPTER 6. EVALUATION
With the COM order strategy, one can effectively test constructor and observer methods. However, since tournaments API has more than one constructor, the order in which
each constructor is tested will also have an effect on the test outcome. If the constructor
enrolling a new player in a tournament is tested first, there will be no tournament in the
system, therefore, it will fail. If the order is reversed, i.e. the tournament constructor is
tested first, the test success will only depend on the player being stored in the microservice data base. These limitations will be further addressed in the next chapter, namely
when discussing the improvement possibilities and the future work.
Listing 6.3 depicts the tournaments application testing results when testing it with
CMO order strategy. Just like in the previous test, there are several operations whose test
result is failed (as expected), namely, the operation responsible for updating a tournament
resource. This happens as a result of the tournament deletion being tested before the
tournament update and, consequently, the tournament does not exist in the system.
>>> Player s API Results:
OK : 6
NOT OK : 0
INCONCLUSIVE : 0
--------------------------------------------------------------------------
>>> Tournament s API Results:
OK : 9
NOT OK : 0
INCONCLUSIVE : 1
Listing 6.3: Specification test results when executing PETIT with CMO order strategy.
By analysing PETITs output, one can see that there is one operation whose test is
inconclusive. Through analysing each operations output, the inconclusive operation test
is identified, and depicted in listing 6.4. In this case, the operation responsible for retrieving a tournament fails even though all preconditions hold. This happens as a result
of mutators being tested before observers, and the tournament deletion operation being
implemented according to its specification. Therefore, trying to retrieve the tournament
that was previously deleted will result in the tournament not being found, which, in this
case, is considered the correct behaviour.
>> PUT /tournaments /{ tournamentId}
> Verifying Invariants : OK
> Recycling Data : OK
> Verifying Preconditions : NOT OK
> Failed:
- response_code(GET /tournaments /2) == 200
> Performing Request : FAILED (as expected)
> Caused by:
> Code: 404
> Message: Tournament with id 2 not found.
52
6.2. TESTING MUTATORS
--------------------------------------------------------------------------
PUT /tournaments /{ tournamentId} : OK
>> GET /tournaments /{ tournamentId}
> Verifying Invariants : OK
> Recycling Data : OK
> Verifying Preconditions : OK
> Performing Request : FAILED (analyse exec. trace)
> Caused by:
> Code: 404
> Message: Tournament with id 2 not found.
--------------------------------------------------------------------------
GET /tournaments /{ tournamentId} : INCONCLUSIVE
Listing 6.4: PETITs partial output of a tournaments API test executed with CMO strategy.
As previously referred, both this strategies can be used to test mutator and observer
operations. As such, CMO strategy can be used to test mutators and COM can also be
used to test observers.
In the first testing scenario, although the specification test results are positive, by
looking into each operation test result, one can conclude that not all possible outcomes
were tested. In the second testing scenario, on the other hand, there is an inconclusive test
case that is not, necessarily, wrong. Ultimately, what both these scenarios aim to enforce
is that one should perceive PETITs output in a critical perspective, not only looking into
the specification test results as a whole, but also into each operation result and the order
in which they were tested.
6.2 Testing Mutators
Testing mutators for its success case will fall into the previously discussed order strategy,
CMO. This happens because in order for mutator operations to perform correctly they
need to work on previously existing resources. This means that, assuming constructors
and observers are correctly implemented, mutators input will be correctly defined and
its effects will be noticeable when testing observers. However, there is still the need to
test these operations when the test data is not conforming to their contract. PETIT is able
to do this when provided with MCO or MOC order strategies. Testing the tournaments
application specification with MCO order strategy produces the same results as the ones
shown in listing 6.3.
Listing 6.5 depicts players API mutator operations results. Since mutator operations
are the first to be tested, there is no data to be updated nor removed. As seen on listing 6.5, the preconditions for both operations updating and removing a player fail.
Since tournaments application is implemented according to its specification, the request
53
CHAPTER 6. EVALUATION
fails, as expected, and the operations testing results are positive.
>> PUT /players /{ playerNIF}
> Verifying Invariants : OK
> Recycling Data : OK
> Verifying Preconditions : NOT OK
> Failed:
- response_code(GET /players /212145124) == 200
> Performing Request : FAILED (as expected)
> Caused by:
> Code: 404
> Message: Player with NIF 212145124 not found.
--------------------------------------------------------------------------
PUT /players /{ playerNIF} : OK
>> DELETE /players /{ playerNIF}
> Verifying Invariants : OK
> Recycling Data : OK
> Verifying Preconditions : NOT OK
> Failed:
- response_code(GET /players /270771533) == 200
> Performing Request : FAILED (as expected)
> Caused by:
> Code: 404
> Message: Player with NIF 270771533 not found.
--------------------------------------------------------------------------
DELETE /players /{ playerNIF} : OK
Listing 6.5: PETITs partial output of a players API test executed with MCO strategy.
The tournaments API mutators operations testing results are similar to the ones of
players API. However, listing 6.3 shows that there was an inconclusive test for a tournaments API operation. The operation whose test is inconclusive is the one responsible for
checking whether a player is enrolled in a tournament. By analysing the test sequence,
shown in listing 6.6, the reason is clear: the operation responsible for inserting an enrollment was tested first, meaning there was still no tournament stored in the system; the
execution proceeds with inserting a tournament and then with checking if a player is enrolled in the tournament that was just inserted. PETIT classifies this test as inconclusive
because it lacks information about the execution trace. By analysing it, one can state that
the microservice behaviour was, in fact, correct.
By being able to detect the previously described test case, one can conclude that this
order strategy could simultaneously be used to test constructor operations.
Listing 6.7 shows the results of testing the tournaments application with MOC order
strategy. As seen in the listing, both players and tournaments APIs have one inconclusive
operation test.
54
6.2. TESTING MUTATORS
>> POST /tournaments /{ tournamentId }/ enrollments
> Verifying Invariants : OK
> Generating Data : OK
> Verifying Preconditions : NOT OK
> Failed:
- response_code(GET /tournaments /46) == 200
> Performing Request : FAILED (as expected)
> Caused by:
> Code: 404
> Message: Tournament with ID 46 not found.
--------------------------------------------------------------------------
POST /tournaments /{ tournamentId }/ enrollments : OK
>> POST /tournaments
> Verifying Invariants : OK
> Generating Data : OK
> Verifying Preconditions : OK
> Performing Request : OK
> Verifying Postconditions : OK
--------------------------------------------------------------------------
POST /tournaments : OK
>> GET /tournaments /{ tournamentId }/ enrollments /{ playerNIF}
> Verifying Invariants : OK
> Recycling Data : OK
> Verifying Preconditions : OK
> Performing Request : FAILED (analyse exec. trace)
> Caused by:
> Code: 404
> Message: Player with NIF 220810071 is not enrolled in the tournament 2.
--------------------------------------------------------------------------
GET /tournaments /{ tournamentId }/ enrollments /{ playerNIF} : INCONCLUSIVE
Listing 6.6: PETITs partial output of a tournaments API test executed with MCO strategy.
>>> Player s API Results:
OK : 5
NOT OK : 0
INCONCLUSIVE : 1
--------------------------------------------------------------------------
>>> Tournament s API Results:
OK : 9
NOT OK : 0
INCONCLUSIVE : 1
Listing 6.7: Specification test results when executing PETIT with MOC order strategy.
The operations whose test result is inconclusive are the ones responsible for retrieving
a player and a tournament resource. Since the PETIT is executed with MOC, the observer
55
CHAPTER 6. EVALUATION
operations are tested before the resources are inserted, therefore, the resources are not
found. PETIT cannot identify this test case as being failed (as expected) as a result of both
these operations preconditions being very permissive, as shown in listings 6.8 and 6.9.
Since preconditions do not fail, PETIT classifies the tests as inconclusive.
1 "/players/{playerNIF}":
2 get:
3 summary: Return a player by NIF .
4 xr e q u i r e s :
5 - T
6 xensures :
7 - T
Listing 6.8: YAML partial object for Players API get player operation.
1 "/tournaments/{tournamentId}":
2 get:
3 summary: Return a tournament by ID .
4 xr e q u i r e s :
5 - T
6 xensures :
7 - T
Listing 6.9: YAML partial object for Tournaments API get tournament operation.
The MOC order strategy not only can be used to test mutators in a failure scenario
but also observers in the same scenario, as shown in the previous example.
Players API mutator operations have the same test results as the previous execution
with MCO strategy. However, tournaments API test results do not show the operation
responsible for checking whether a player is enrolled in a tournament classified as inconclusive, since, this time, neither the player nor the tournament exist. As such, both
operations preconditions fail and the test result is failed (as expected) and the operations
implementation classified as being according to the specification, i.e., ok.
6.3 Testing Observers
Testing tournaments application with both OMC and OCM order strategies the test results are the same as the ones described in the previous section section 6.2 when
testing it with MOC strategy. Both APIs have an inconclusive operation test and it happens to be the same ones retrieving a player and a tournament , for the exact same
reasons.
Testing observers immediately before constructors, assuming constructors are implemented according to its specification, one should check if the previously inserted
resources are, in fact, shown. Testing observers immediately after mutators, assuming
56
6.4. TOURNAMENTS APPLICATION: FAULTY SCENARIO
mutators implementation is according to its specification, one should look for discrepancies on whether what was modified by the mutators is shown when testing observers.
Hereupon, every single operation order strategy is equally useful to test observer operations.
6.4 Tournaments Application: faulty scenario
As mentioned in the beginning of this chapter, there is the need to test PETIT in a faulty
application in order to figure out if it is capable of finding out if a microservices implementation is, in fact, according to its specification. This sections listings depict PETITs
output when executed only in verbose mode v flag. Once more, the tournaments application is used as a base example, and as such, several implementation errors are added
to its implementation. The new implementation of tournaments application features six
different errors:
Tournament Deletion the specification states that if all preconditions hold then the microservice will return the tournament that was removed from the system. In this
case, instead of returning the resource, the microservice returns null.
Enrollment Deletion the player is not disenrolled from the tournament.
Tournament Insertion the tournament is inserted with missing information.
Tournament Update the tournament supposed to be updated remains the same as it was
before.
Player Insertion the player is not stored in the system. Listing 6.10 depicts PETITs
output in this scenario, executed with COM strategy. By checking the operation
postcondition results, one can conclude that the player was not, in fact, stored in
the system.
>> POST /players
> Verifying Invariants : OK
> Generating Data : OK
> Verifying Preconditions : OK
> Performing Request : OK
> Response
{ "playerNIF": "259447224",
"firstName": "PEbz N0_YPWtB80uy0uDvWCu7A0McI -PnW0zgRAmW",
"lastName": "ffxY7 u__vJSl0bWfESYlJCEhkd5PPNEG",
"address": "v58FjjkPCnB5etMka59kstZnuDYWx13rBNDVCRzJFmmJcKv",
"email": "6_-_.9@g.B",
"phone": "291956980",
"tournaments": []
}
57
CHAPTER 6. EVALUATION
> Verifying Postconditions : NOT OK
> Failed:
- response_code(GET /players /259447224) == 200
------------------------------------------------------------------------
POST /players : NOT OK
Listing 6.10: PETITs test results for the faulty player insertion.
Player Deletion the wrong player gets deleted. Listing 6.11 shows PETIT result for this
operations test, when executed with CMO order strategy. This operations specification states that it should retrieve the player that got deleted. However, by analysing
PETITs output one can see that the retrieved player was not the one supposed to
be deleted, as shown by the second postconditions results. The first postcondition
states that after deletion, the player should not be found and, also fails because the
wrong player got deleted.
>> DELETE /players /{ playerNIF}
> Verifying Invariants : OK
> Recycling Data : OK
> Verifying Preconditions : OK
> Performing Request : OK
> Response
{ "playerNIF": "100123123",
"firstName": "ana",
"lastName": "ribeiro",
"address": "rua 1",
"email": "ana@ana.ana",
"phone": "999999999",
"tournaments": [
{ "tournamentId": 1,
"tournamentName": "Triwizzard Tournament 2020",
"capacity": 3,
"playerNumber": 0,
"players": []
}
]
}
> Verifying Postconditions : NOT OK
> Failed:
- response_code(GET /players /158536692) == 404
- response_body(this)== previous(response_body(GET /players /158536692)
------------------------------------------------------------------------
DELETE /players /{ playerNIF} : NOT OK
Listing 6.11: PETITs test results for the faulty player deletion.
In order to find the relationship between operation order and error detection PETIT
was subject to several tests. Table 6.1 depicts the tests results. As seen in table 6.1, not
58
6.4. TOURNAMENTS APPLICATION: FAULTY SCENARIO
CMO COM MCO MOC OCM OMC
Player Deletion ✓ ✓ × ××
Tournament Deletion ✓ ✓ × ××
Enrollment Deletion ✓ ✓ × × ✓ ✓
Player Insertion ✓ ✓ ✓ ✓ ✓ ✓
Tournament Insertion ✓ ✓ ✓ ✓ ✓ ✓
Tournament Update ✓ ✓ × ××
Table 6.1: Error detection in each order strategy.
every order strategy detects every error. By only analysing the table it may seem that
PETIT is not very good when testing mutator operations. Considering only the failing
cells, i.e. the ones with ×, one can see that the error is not detected because the operation
order is not suitable for testing mutators for their success scenario. In every single time
PETIT did not detect an error on a mutator operation, the strategy chosen always tested
mutators before constructors and, consequently, there was no sufficient data to find the
implementation errors.
59
C h a p t e r
7
Conclusions and Future Work
This chapter features this works conclusions as well as the possible future improvements
to PETIT and APOSTL.
7.1 Conclusions
PETIT aPi tEsTIng Tool is developed with the purpose of automating the microservice
testing process. Its implementation falls into black-box testing, more precisely, into
the specification-based testing approach. As such, PETIT only needs the microservices
specification in order to be able to test them. Although these specifications have useful
information, there is still the need to complement it with more information so the testing
could be thorougher. APOSTL API PrOperty SpecificaTion Language is developed
for this purpose and, as the name implies, is a language developed to formally annotate
APIs with properties that will, ultimately, constitute an API contract.
Nowadays the industry is dangerously migrating to microservice architectures without a reliable and automated process for effectively testing the software it is using. This
thesis contributions work towards the mitigation this problem, contributing not only
with a specification language purposely built to formally specify microservices API contracts, but also with a testing tool capable of generating (non-redundant) test data, and
automatically testing the microservices implementation.
Several tests are conducted in order to ascertain whether PETITs behaviour is according to what is expected. PETIT is tested against a correct and a faulty application. The test
results on the correct application have shown that although PETITs output concerning
the whole specification is positive, there is still the need to analyse the entirety of the
execution trace. This need arises from the fact that an operation should be tested for its
every possible outcome. As shown in chapter 6, that is, usually, not the case with a single
61
CHAPTER 7. CONCLUSIONS AND FUTURE WORK
PETIT execution. The tests conducted in the faulty application are positive, meaning
PETIT is able to find every introduced error, when provided with the appropriate order
strategy. The test results also shown that the order strategy parameter should be carefully
considered when using PETIT.
To summarize, the contributions initially planned were successfully achieved. This
work contributions are an API specification language developed to specify API contracts,
an algorithm which automatically generates test data for microservices, based on their
extended specification, and, finally, a tool integrating both of these features and automating the microservice testing process. However, the language, the algorithm, and the tool
itself can be improved. At this stage, neither PETIT nor APOSTL are developed at their
highest potential.
7.2 Future Work
As previously referred, both PETIT and APOSTL implementations have room for improvement. In the current implementation, PETIT is only able to test an operation once
per execution. It is important that, in the future, PETIT is able to test operations several
times during a single execution to, e.g., test numerical invariants such as the one depicted
in listing 5.2. In PETITs current implementation there is no way to test the previous
invariant when the capacity property is greater than 1, since the operation responsible
for inserting a tournament is not tested more than once, and every test data is deleted
from the database when PETITs execution is over, i.e., assuming deletion operations are
implemented conforming to their specification.
PETIT should also be able to test each API operation independently. Currently, the
only way a user can manipulate the operations being tested is by changing the API testing
order r flag or the operation order strategy. Besides having control on the operation
order, users should also have control on which operations are being, in fact, tested.
APOSTLs implementation can also be enhanced by improving expressiveness. This
can be achieved by changing APOSTLs grammar in order to accept properties such as
nested quantifiers, as described in section 5.1.4. APOSTL is a specification language
that can be used with any API description language that supports being extended. Currently, PETIT only supports OAS but it can also support other common used description
languages such as RAML [42] RESTful API Modeling Language.
62
References
[1] V. T. Vasconcelos, F. Martins, A. Lopes, and N. Burnay. “HeadREST: A Specification
Language for RESTful APIs”. In: Models, Languages, and Tools for Concurrent and
Distributed Programming: Essays Dedicated to Rocco De Nicola on the Occasion of His
65th Birthday. Ed. by M. Boreale, F. Corradini, M. Loreti, and R. Pugliese. Springer
International Publishing, 2019, pp. 428434. doi: 10.1007/978- 3- 030- 21485-
2_23.
[2] C. A. R. Hoare. “An Axiomatic Basis for Computer Programming”. In: Commun.
ACM 12.10 (Oct. 1969), 576580. issn: 0001-0782. doi: 10.1145/363235.363259.
[3] B. Meyer. “Applying design by contract’”. In: Computer 25.10 (1992), pp. 4051.
issn: 1558-0814. doi: 10.1109/2.161279.
[4] R. W. Floyd. “Assigning Meanings to Programs”. In: Program Verification: Fundamental Issues in Computer Science. Ed. by T. R. Colburn, J. H. Fetzer, and T. L.
Rankin. Dordrecht: Springer Netherlands, 1993, pp. 6581. doi: 10.1007/978-94-
011-1793-7_4.
[5] E. W. Dijkstra. A Discipline of Programming. Prentice-Hall, 1976.
[6] G. J. Myers, C. Sandler, and T. Badgett. The art of software testing. John Wiley &
Sons, 2011.
[7] C. S. Glenford J. Myers Tom Badget. The Art of Software Testing. John Wiley & Sons,
Inc., 2012.
[8] S. Anand, E. K. Burke, T. Y. Chen, J. Clark, M. B. Cohen, W. Grieskamp, M. Harman,
M. J. Harrold, P. McMinn, A. Bertolino, J. J. Li, and H. Zhu. “An orchestrated
survey of methodologies for automated software test case generation”. In: Journal
of Systems and Software 86.8 (2013), pp. 1978 2001. issn: 0164-1212. doi: j.jss.
2013.02.061.
[9] D. Shadija, M. Rezai, and R. Hill. “Towards an understanding of microservices”.
In: 2017 23rd International Conference on Automation and Computing (ICAC). 2017,
pp. 16. doi: 10.23919/IConAC.2017.8082018.
[10] R. Hamlet. “Random Testing”. In: Encyclopedia of Software Engineering. American
Cancer Society, 2002. doi: 10.1002/0471028959.sof268.
63
REFERENCES
[11] K. Meinke, F. Niu, and M. A. Sindhu. “Learning-Based Software Testing: A Tutorial”. In: Leveraging Applications of Formal Methods, Verification, and Validation
- International Workshops, SARS 2011 and MLSC 2011, Held Under the Auspices of
ISoLA 2011 in Vienna, Austria, October 17-18, 2011. Revised Selected Papers. Ed. by
R. Hähnle, J. Knoop, T. Margaria, D. Schreiner, and B. Steffen. Vol. 336. Communications in Computer and Information Science. Springer, 2011, pp. 200219. doi:
10.1007/978-3-642-34781-8\_16.
[12] K. Meinke. “CGE: A Sequential Learning Algorithm for Mealy Automata”. In:
Grammatical Inference: Theoretical Results and Applications, 10th International Colloquium, ICGI 2010, Valencia, Spain, September 13-16, 2010. Proceedings. Ed. by J. M.
Sempere and P. García. Vol. 6339. Lecture Notes in Computer Science. Springer,
2010, pp. 148162. doi: 10.1007/978-3-642-15488-1\_13.
[13] K. Meinke and M. A. Sindhu. “Incremental Learning-Based Testing for Reactive
Systems”. In: Tests and Proofs - 5th International Conference, TAP 2011, Zurich,
Switzerland, June 30 - July 1, 2011. Proceedings. Ed. by M. Gogolla and B. Wolff.
Vol. 6706. Lecture Notes in Computer Science. Springer, 2011, pp. 134151. doi:
10.1007/978-3-642-21768-5\_11.
[14] T. Y. Chen, F.-C. Kuo, R. G. Merkel, and T. Tse. “Adaptive Random Testing: The
ART of test case diversity”. In: Journal of Systems and Software 83.1 (2010). SI: Top
Scholars, pp. 60 66. issn: 0164-1212. doi: 10.1016/j.jss.2009.02.022.
[15] T. Y. Chen, R. Merkel, P. K. Wong, and G. Eddy. “Adaptive random testing through
dynamic partitioning”. In: Fourth International Conference on Quality Software,
2004. QSIC 2004. Proceedings. 2004, pp. 7986. doi: 10 . 1109 / QSIC . 2004 .
1357947.
[16] H. Liu, X. Xie, J. Yang, Y. Lu, and T. Y. Chen. “Adaptive random testing through
test profiles”. In: Software: Practice and Experience 41.10 (2011), pp. 11311154.
doi: 10.1002/spe.1067.
[17] T. Y. Chen, F.-C. Kuo, and H. Liu. “Adaptive random testing based on distribution
metrics”. In: Journal of Systems and Software 82.9 (2009), pp. 1419 1433. issn:
0164-1212. doi: 10.1016/j.jss.2009.05.017.
[18] T. Y. Chen, F.-C. Kuo, and R. Merkel. “On the statistical properties of testing
effectiveness measures”. In: Journal of Systems and Software 79.5 (2006). Quality
Software, pp. 591 601. issn: 0164-1212. doi: 10.1016/j.jss.2005.05.029.
[19] I. Ciupa, A. Leitner, M. Oriol, and B. Meyer. “ARTOO: Adaptive Random Testing
for Object-Oriented Software”. In: Proceedings of the 30th International Conference
on Software Engineering. ICSE 08. Leipzig, Germany: Association for Computing
Machinery, 2008, 7180. doi: 10.1145/1368088.1368099.
64
REFERENCES
[20] Y. Lin, X. Tang, Y. Chen, and J. Zhao. “A Divergence-Oriented Approach to Adaptive Random Testing of Java Programs”. In: Proceedings of the 2009 IEEE/ACM
International Conference on Automated Software Engineering. ASE 09. USA: IEEE
Computer Society, 2009, 221232. doi: 10.1109/ASE.2009.13.
[21] J. Mayer. “Lattice-Based Adaptive Random Testing”. In: Proceedings of the 20th
IEEE/ACM International Conference on Automated Software Engineering. ASE 05.
Long Beach, CA, USA: Association for Computing Machinery, 2005, 333336. doi:
10.1145/1101908.1101963.
[22] A. Shahbazi, A. F. Tappenden, and J. Miller. “Centroidal Voronoi Tessellations - A
New Approach to Random Testing”. In: IEEE Transactions on Software Engineering
39.2 (2013), pp. 163183. issn: 2326-3881. doi: 10.1109/TSE.2012.18.
[23] A. F. Tappenden and J. Miller. “A Novel Evolutionary Approach for Adaptive
Random Testing”. In: IEEE Transactions on Reliability 58.4 (2009), pp. 619633.
issn: 1558-1721. doi: 10.1109/TR.2009.2034288.
[24] K. Claessen and J. Hughes. “QuickCheck: A Lightweight Tool for Random Testing
of Haskell Programs”. In: SIGPLAN Not. 46.4 (May 2011), 5364. issn: 0362-1340.
doi: 10.1145/1988042.1988046.
[25] J. W. Duran and S. C. Ntafos. “An Evaluation of Random Testing”. In: IEEE
Transactions on Software Engineering SE-10.4 (1984), pp. 438444. issn: 2326-3881.
doi: 10.1109/TSE.1984.5010257.
[26] Y. Cheon. “Automated Random Testing to Detect Specification-Code Inconsistencies”. In: International Conference on Software Engineering Theory and Practice, SETP07, Orlando, Florida, USA, July 9-12 2007. Ed. by D. A. Karras, D. Wei, and J. Zendulka. ISRST, 2007, pp. 112119. url: https:/ /dblp.org /rec/conf /setp/
Cheon07.bib.
[27] Y. Cheon and C. E. Rubio-Medrano. “Random Test Data Generation for Java Classes
Annotated with JML Specifications”. In: Proceedings of the 2007 International Conference on Software Engineering Research & Practice, SERP 2007, Volume II, June 25-28,
2007, Las Vegas Nevada, USA. Ed. by H. R. Arabnia and H. Reza. CSREA Press,
2007, pp. 385391. url: https://dblp.org/rec/conf/serp/CheonR07.bib.
[28] C. Boyapati, S. Khurshid, and D. Marinov. “Korat: automated testing based on Java
predicates”. In: Proceedings of the International Symposium on Software Testing and
Analysis, ISSTA 2002, Roma, Italy, July 22-24, 2002. Ed. by P. G. Frankl. ACM, 2002,
pp. 123133. doi: 10.1145/566172.566191.
[29] T. Parr. The Definitive ANTLR 4 Reference. 2nd. Pragmatic Bookshelf, 2013. isbn:
1934356999.
65
Online references
[30] M. Fowler. Software Testing Guide. Accessed in January 2020. 2019. url: https:
//martinfowler.com/testing/.
[31] M. Fowler and J. Lewis. Microservices. Accessed in January 2020. 2014. url: http:
//martinfowler.com/articles/microservices.html.
[32] OpenAPI Specification. Accessed in January 2020. url: https : / / swagger . io /
solutions/getting-started-with-oas/.
[33] OpenAPI Initiative. Accessed in January 2020. url: https://www.openapis.org/
about.
[34] Swagger PetStore Example. Accessed in January 2020. url: https : / / petstore .
swagger.io/.
[35] OpenAPI Documentation. Accessed in September 2020. url: https://swagger.
io/specification/#document-structure.
[36] cURL. Accessed in January 2020. url: https://curl.haxx.se/docs/manpage.
html.
[37] Postman. Accessed in January 2020. url: https://learning.getpostman.com/
docs/postman/launching-postman/introduction/.
[38] Dredd. Accessed in January 2020. url: https://dredd.org/en/latest/how-itworks.html.
[39] Swagger: Data Models. Accessed in January 2020. url: https : / / swagger . io /
docs/specification/data-models.
[40] Postman: Scripts. Accessed in January 2020. url: https://learning.getpostman.
com/docs/postman/scripts/test-scripts/.
[41] J. Dziworski. Listener vs Visitor. Accessed in June 2020. 2016. url: http : / /
jakubdziworski.github.io/java/2016/04/01/antlr_visitor_vs_listener.
html.
[42] RAML - RESTful API Modeling Language. Accessed in October 2020. url: https:
//raml.org/.
67