Tired of writing over and over the same unit tests for testing Retrofit interfaces? Exhausted of asserting for success and failure response depending on the request? Irritated of implementing manually every Retrofit interface to mock server's behaviour? One step away from kill yourself just to get some sort of freedom?
Probably not, because you don't give a shit about all this. You don't test your network layer, neither you don't mock it, and of course you never listen to your mother. And that's good. That's fair enough. You should not do anything that could kill your delicate and delightful spirit.
You should write a library and that library should do it for you. A library should exists and this library should generate unit tests and mock server responses based on simple rules. That was the thought, and the thought became nothing just like most good ideas.
But I wrote that library -for you, for me, for all that children that cry at harsh nights when not unit tests come. That library is this library, and it is called Mockery and it generates all that crappy boring tests for you, and it mocks the server behaviour too. You just need to decorate the old good Retrofit interfaces with a bunch of annotations. It's not so hard.
Mockery is designed for testing and mocking networking layers, helping to mock DTOs and auto-generating unit tests to ensure that the contract between the client application and API is fulfilled. For that, Mockery operates as follows:
- Mock server responses using Java
interfaces
andannotations
. - Validate server responses using Java
interfaces
,annotations
and JUnit.
Add JitPack repository in your build.gradle (top level module):
allprojects {
repositories {
jcenter()
maven { url "https://jitpack.io" }
}
}
Depending on your needs, add one of the next dependencies in the build.gradle script of your target module:
Mockery supporting Retrofit with responses of type Call<T>
:
dependencies {
compile 'com.github.VictorAlbertos.Mockery:extension_retrofit:1.0.2'
}
Mockery supporting Retrofit with responses of type Single<T>
, Single<Response<T>>
and Completable
:
dependencies {
compile 'com.github.VictorAlbertos.Mockery:extension_rx2_retrofit:1.0.2'
}
Once selected the dependency for mocking responses, add next dependency using android-apt plugin so Mockery can generate the unit tests that will stand up as the contract between your client application and the API:
Root build.gradle script:
dependencies {
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
Target module build.gradle script:
apply plugin: 'com.neenbedankt.android-apt'
dependencies {
apt 'com.github.VictorAlbertos.Mockery:test_compiler:1.0.2'
provided 'com.github.VictorAlbertos.Mockery:test_runtime:1.0.2'
provided 'org.glassfish:javax.annotation:10.0-b28'
provided 'junit:junit:4.12'
}
Create an interface
with as much methods as needed to gather the API endpoints. This interface
needs to be annotated with one of the following @Interceptor annotations:
- @Retrofit
- @Rx2Retrofit
Next interface
is decorated with @Retrofit annotation
, which induces Mockery to take care of every aspect related with mocking/validating responses created by Retrofit when using Call<T>
type. Mockery behaves the same way as an instance of Retrofit does, regarding threading and http exceptions. Actually, the interface
supplied to Retrofit builder should be the same that the one supplied to Mockery (you can thanks to Jake Wharton for this).
@Retrofit(delay = 2500, failurePercent = 15)
public interface RestApi {
@GET("/users/{username}")
@DTOArgs(UserDTO.class)
Call<User> getUserByName(@Valid(STRING) @Path("username") String username);
@GET("/users")
@DTOArgs(UsersDTO.class)
Call<List<User>> getUsers(@Optional @Query("since") int lastIdQueried,
@Optional @Query("per_page") int perPage);
@GET("/users/{username}/repos")
@DTO(ReposDTO.class)
Call<List<Repo>> getRepos(
@Valid(STRING) @Path("username") String username,
@Enum({"all", "owner", "member"}) @Query("type") String type,
@Enum({"asc", "desc"}) @Query("direction") String direction);
}
@Retrofit annotation
accepts 4 optional params to configure the network behaviour:
- delay: set the network's round trip delay in milliseconds
- failurePercent: set the percentage of calls to fail.
- variancePercentage: set the plus-or-minus variancePercentage percentage of the network round trip delay
- errorResponseAdapter: adapt the error message from a failure response to mimic the expected one returned by the server.
As long as the @Retrofit annotation
, the previous interface has been decorated with Mockery annotations to define the way Mokcery mocks and tests the endpoint (more here).
For a complete Retrofit example using Call<T>
, there is an android module dedicated to it.
Next interface
is decorated with @Rx2Retrofit annotation
, which inducts Mockery to behave in a similar way that it does when it is annotated with @Retrofit annotation
, but with the difference that the response type is encapsulated in an Single<T>
, Single<Response<T>>
or Completable
.
@Rx2Retrofit(delay = 2500, failurePercent = 15)
public interface RestApi {
@PUT("/users/{username}")
@NoDTO Completable addUser(@Valid(value = STRING) @Path("username") String username);
@GET("/users/{username}")
@DTOArgs(UserDTO.class)
Single<Response<User>> getUserByName(@Valid(STRING) @Path("username") String username);
@GET("/users")
@DTOArgs(UsersDTO.class)
Single<List<User>> getUsers(@Optional @Query("since") int lastIdQueried,
@Optional @Query("per_page") int perPage);
@GET("/users/{username}/repos")
@DTO(ReposDTO.class)
Single<Response<List<Repo>>> getRepos(@Valid(STRING) @Path("username") String username,
@Enum({"all", "owner", "member"}) @Query("type") String type,
@Enum({"asc", "desc"}) @Query("direction") String direction);
}
As @Retrofit annotation, @Rx2Retrofit accepts the same values to configure the behaviour of the server responses.
For a complete Retrofit example using RxJava2, there is another android module dedicated to it.
After being done with decorating the RestApi interface
, instantiate it using Mockery.Builder<T>
:
if (BuildConfig.DEBUG) {
restApi = new Mockery.Builder<RestApi>()
.mock(RestApi.class)
.build();
} else {
restApi = //real implementation, probably using Retrofit.
}
For every interface
annotated with Mockery annotations a new java class
is generated with as much unit tests as needed to fulfil the requirements expressed by the Mockery annotations.
The name of the test generated is the same as the interface
from which is generated, but appending a Test_ suffix. So, an interface called RestApi, generates a class called RestApiTest_. This class is abstract
, from which you need to extend and implement the only one abstract
method to provide an instance with the real interface
implementation.
For example, this interface
:
@Retrofit
public interface RestApi {
@GET("/users/{username}")
@DTOArgs(UserDTO.class) Call<User> getUserByName(
@Valid(value = STRING, legal = "google") @Path("username") String username);
}
Generates this abstract
class:
@RunWith(OrderedRunner.class)
public abstract class RestApiTest_ {
@Rule
public final ExpectedException exception = ExpectedException.none();
protected abstract RestApi restApi();
@Test
@Order(0)
public void When_Call_getUserByName_With_Illegal_username_Then_Get_Exception() {
// Init robot tester
Robot robot = RobotBuilder
.test(RestApi.class)
.onMethod("getUserByName")
.build();
// Declare value params
String username = robot.getIllegalForParam(0);
// Perform and validate response
Call<User> response = restApi().getUserByName(username);
exception.expect(AssertionError.class);
robot.validateResponse(response);
}
@Test
@Order(0)
public void When_Call_getUserByName_Then_Get_Response() {
// Init robot tester
Robot robot = RobotBuilder
.test(RestApi.class)
.onMethod("getUserByName")
.build();
// Declare value params
String username = robot.getLegalForParam(0);
// Perform and validate response
Call<User> response = restApi().getUserByName(username);
robot.validateResponse(response);
}
}
And to use it, you just need to extend from it and run the test, as follows:
public final class RestApiTest extends RestApiTest_ {
@Override protected RestApi restApi() {
return new Retrofit.Builder()
.baseUrl("whatever")
.build().create(RestApi.class);
}
}
The generated code hides its internal details using some sort of Robot pattern, which provides a cleaner legibility of the generated code. Plus, the order in which the tests are executed is based on the position at which the methods were declared in the original interface
.
If for some reason an specific method requires to not generate its companion test, just annotate it with @SkipTest
and Mockery will skip it.
@Retrofit
public interface RestApi {
@GET("/users/{username}")
@SkipTest
@DTOArgs(UserDTO.class) Call<User> getUserByName(
@Valid(value = STRING, legal = "google") @Path("username") String username);
}
To configure Mockery for mocking and validating server responses, you need to decorate the interface
with Mockery annotations. Either using the built-in ones or creating a custom one.
Every Mockery annotation has two functions: supply mock objects and validate that some data meets certain criteria. And these functions are expressed in two dimensions: the production code, where Mockery mimics the server behaviour validating the parameter values received and serving a response; and the test code, where Mockery tests the server behaviour sending the parameter values and validating its responses.
Following Mockery annotations are accessible as built-in parts of Mockery's core.
- Target:
method
. - Arguments: none.
- When to use: when performing calls with
Completable
. - How to use: decorate the method/param with
@NoDTO
. - Usage example:
//Decorate a method with @DTO.
@NoDTO Completable putUser(String username);
- Target:
method
andparam
. - Arguments: a
Class
which implements DTO.Behaviourinterface
. - Supported types: the specified generic
type
ofDTO.Behaviour<T>
interface
, including parameterized types likeList<T>
,Map<K,V>
, and so on. - When to use: to mock or validate a custom DTO.
- How to use: decorate the method/param with
@DTO
supplying aDTO.Behaviour<T>
implementation. - Usage example:
//Implement DTO.Behaviour<T> parameterizing the desired model.
public class ReposDTO implements DTO.Behaviour<List<Repo>> {
@Override public List<Repo> legal() {
//Populate list
}
@Override public void validate(List<Repo> candidate) throws AssertionError {
//Validate list
}
}
//Decorate a method or param with @DTO.
@DTO(ReposDTO.class)
List<Repo> getRepos(@DTO(ReposDTO.class) List<Repo>);
- Target:
method
. - Arguments: a
Class
which implements DTOArgs.Behaviourinterface
. - Supported types: the specified generic type of
DTOArgs.Behaviour<T>
interface
, including parameterized types likeList<T>
,Map<K,V>
, and so on. - When to use: to mock or validate a custom DTO and it is required to access the array of objects representing the values supplied in the method call invocation.
- How to use: decorate the method with
@DTOArgs
supplying aDTOArgs.Behaviour<T>
implementation. - Usage example:
//Implement DTOArgs.Behaviour<T> parameterizing the desired model.
public class ReposDTO implements DTOArgs.Behaviour<List<Repo>> {
@Override public List<Repo> legal(Object[] args) {
int perPage = (int) args[0];
//Populate list
}
@Override public void validate(List<Repo> candidate) throws AssertionError {
//Validate list
}
}
//Decorate a method with @DTOArgs.
@DTOArgs(ReposDTO.class)
List<Repo> getRepos(int perPage);
- Target:
param
. - Arguments: a
Class
which implementsDTO.Behaviour<T>
interface. - Supported types:
String
. - When to use: to mock or validate a custom DTO that is serialized as a json
String
representation. - How to use: @DTOJson serialize-deserialize the associated object from-to json, and because of that, it requires that the interface using it is annotated with JsonConverter, which accepts a JolyglotGeneric instance. To use it, decorate the param with
@DTOJson
, supplying aDTO.Behaviour<T>
implementation. - Usage example:
//Implement DTO.Behaviour<T> parameterizing the desired model.
public class ReposDTO implements DTO.Behaviour<List<Repo>> {
@Override public List<Repo> legal() {
//Populate list
}
@Override public void validate(List<Repo> candidate) throws AssertionError {
//Validate list
}
}
//Decorate the interface with @JsonConverter and the param with @DTOJson.
@JsonConverter(GsonSpeaker.class)
interface RestApi {
...(@DTOJson(ReposDTO.class) String reposJson);
}
- Target:
param
. - Arguments:
- value: an
String[]
containing the enumerated values. - legal (optional): an
String
which sets the value to send to the server when running the associated unit test asserting for a success response. If not set, a random value from one of the defined enumerations is used. - illegal (optional): an
String
which sets the value to send to the server when running the associated unit test asserting for a failure response. If not set, an empty string or a 0 value is used, depending on the associatedtype
param.
- value: an
- Supported types:
String
,Character
,double
,Double
,float
,Float
,int
,Integer
,long
,Long
. - When to use: to mock or validate a serie of enumerated values.
- How to use: decorate the param with
@Enum
supplying an String[] containing the enumerated sequence. - Usage example:
...(@Enum({"all", "owner", "member"}) String type);
...(@Enum({"0", "1", "2"}) int type);
...(@Enum(value = {"all", "owner", "member"}, legal = "owner", illegal = "whatever") String type);
...(@Enum(value = {"0", "1", "2"}, legal = "1", illegal = "0") int type);
- Target:
method
andparam
. - Arguments:
- value: a
String
containing a regular expression. Either use one of the available regular expressions listed in Valid.Template or supply a custom one. - legal (optional): a
String
which sets the value to send to the server when running the associated unit test asserting for a success response. If not set, a random value that matches the supplied regular expression is used. - illegal (optional): a
String
which sets the value to send to the server when running the associated unit test asserting for a failure response. If not set, an empty string or a 0 value is used, depending on the associatedtype
param.
- value: a
- Supported types:
String
,Character
,double
,Double
,float
,Float
,int
,Integer
,long
,Long
. - When to use: to mock or validate values that match certain regular expressions, like email, phone, id and so on.
- How to use: decorate the method/param with
@Valid
supplying the regular expression. - Usage example:
import static io.victoralbertos.mockery.api.built_in_mockery.Valid.Template.EMAIL;
import static io.victoralbertos.mockery.api.built_in_mockery.Valid.Template.NUMBER;
import static io.victoralbertos.mockery.api.built_in_mockery.Valid.Template.ID;
@Valid(NUMBER)
int getCredits(@Valid(EMAIL) String email);
@Valid(ID)
int getId(@Valid(EMAIL) String email);
- Target:
param
. - Arguments:
none
. - Supported types: The same as the
type
of the param. - When to use: for those params which don't need to be mocked or validated because they are described as optionals on the API specs.
- How to use: decorate the param with
@Optional
. - Usage example:
...(@Optional String email, @Optional Model model);
- Target:
param
. - Arguments: a
Class
which implementsDTO.Behaviour<T>
interface. - Supported types:
RequestBody
. - When to use: to mock or validate a custom DTO that is serialized as a json String and encapsulated it in the content of a
RequestBody
. - How to use: decorate the param with
@RequestBodyDTO
supplying aDTO.Behaviour<T>
implementation. - Usage example:
//Implement DTO.Behaviour<T> parameterizing the desired model.
public class ReposDTO implements DTO.Behaviour<List<Repo>> {
@Override public List<Repo> legal() {
//Populate list
}
@Override public void validate(List<Repo> candidate) throws AssertionError {
//Validate list
}
}
//Decorate the param with @RequestBodyDTO.
...(@RequestBodyDTO(ReposDTO.class) RequestBody reposJson);
- Target:
param
. - Arguments: same as @Valid annotation.
- Supported types:
RequestBody
. - When to use: to mock or validate values which match certain regular expressions serialized as the content of a
RequestBody
. - How to use: decorate the param with
@RequestBodyValid
supplying the regular expression. - Usage example:
import static io.victoralbertos.mockery.api.built_in_mockery.Valid.Template.EMAIL;
import static io.victoralbertos.mockery.api.built_in_mockery.Valid.Template.ID;
...(@RequestBodyValid(EMAIL) RequestBody email,
@RequestBodyValid(ID) RequestBody id);
Mockery offers a very extensible API to provide custom mocking/validation specs as such as support for other networking libraries.
First create the desired annotation
. This annotation
has to be annotated with @Mockery annotation
, which tells to Mockery that this annotation
has to be processed as a new component of the library.
@Retention(RUNTIME)
@Target(PARAMETER)
@Mockery(CustomMockery.class)
public @interface Custom {
}
@Mockery annotation
demands as argument a class
which implements Mockery.Behaviour<A>
:
/**
* Define how the annotation should behave.
* @param <A> the type of the associated annotation.
*/
public final class CustomMockery implements Mockery.Behaviour<Custom> {
@Override public Object legal(Metadata<Custom> metadata) {
//Given some criteria, returns a legal value which conforms with that criteria
}
@Override public Object illegal(Metadata<Custom> metadata) {
//Given some criteria, returns an illegal value which does not conform with that criteria
}
@Override public void validate(Metadata<Custom> metadata, Object candidate)
throws AssertionError {
//Validate if the current object meets some criteria. If not, an AssertionError must be thrown
}
@Override public Type[] supportedTypes(Metadata<Custom> metadata) {
//Return an array containing the supported types for this specific implementation. For instance, String.class
}
@Override public boolean isOptional() {
//If true, no unit test asserting for failure would be generated for the param annotated with this annotation;
}
}
Once you have created the annotation
and implemented its behaviour with Mockery.Behaviour<A>
, the annotation
is ready to be used as any other built-in mockery annotation
.
...(@Custom String email);
But before creating any custom annotation
, think about opening an issue to warning about this missing functionality in order to check if Mockery should support this feature as a built-in annotation.
The process of creating a custom Interceptor is very similar to the process of creating a new Mockery annotation. You have to create the desired annotation
and decorate it with @Interceptor annotation
, which demands as argument a class which implements Interceptor.Behaviour to define how the annotation should behave.
But the process to support a new networking library should be done both carefully and with proper testing. For that reason, in case you were willing to add support for a new networking library; please open an issue requesting support and we will try to integrate it as a new built-in extension for Mockery. That way the library will grow in new features to natively support other people's demands.
Víctor Albertos
- https://twitter.com/_victorAlbertos
- https://linkedin.com/in/victoralbertos
- https://github.com/VictorAlbertos
- RxCache: Reactive caching library for Android and Java.
- RxActivityResult: A reactive-tiny-badass-vindictive library to break with the OnActivityResult implementation as it breaks the observables chain.
- RxSocialConnect: OAuth RxJava extension for Android.