Mock factory for Android testing in multi-module system
Very weird and long title. Let me explain…
TL;DR
This post explores how to export object creation factory that could be reused through all over the modules in your Android project. To find out, just scroll down to the #solution. The whole story is a bit below. Also, here is the example project on github.
Baby steps
Here at Treatwell we have many products that solve different problems for our beloved salons and through out the time, we’ve managed to achieve some cool things. Personally, I work on our product Treatwell Connect
for Android platform, that its sole responsibility is to be the best management software for the salon. Through this time we’ve managed to work out stuff that I’ve felt like I would love to share it with the world.
So here it goes.
Problem
When we do any kind of work on Connect
, we try to follow the first scout rule — leave the playground better than it was before. In that sense, to achieve this I mostly lean on unit testing. Also, our app is not that small, so it is split into different modules instead of having one big bundle. By doing this, beside getting more compilation speed, more rapid feedback and happier developers, this also creates some minor inconveniences.
Let me elaborate.
So this is how our app is split (we have even more components, but the main ones are these):
- model
contains all the entity models for all the app and its parts
- components
logical components that have little to do with android framework. Mostly business logic, loaders, display components, utils, formatters etc.
- ui
exported views (did i mention we have quite a bit of complex views?) and their bindings
- app
the whole application that connects (sorry, no pun intended:( ) everything
So as you know a bit more about background, here is the problem. One of the most common things that you do when you are writing unit tests — you create mocks (or face classes with fake data if you please). The problem when having this multi-module environment is, if you create a mock utility class for easier class creation, it cannot be reused in another modules. Its because a) you only would like to have this kind of class(es) only in test environment and b) test environments do not share classes between different modules.
Example
Yep, and here is an example.
So for instance we have this cool app, that we try to assemble. We have:
- app
main app module, every library module connects to this.
- components
logic thingies goes here, uses models
- models
entities
To assemble everything, we need to connect it. So in build.gradle
of the components
module, we insert association to models
module:
dependencies {
…
implementation project(‘:models’)
…
}
And on the app module, we inject every module we will be using.
dependencies {
…
implementation project(‘:components’)
implementation project(‘:models’)
…
}
Now we have a multi module project. Lets create some entities we will be using in models
module. We will use Session
that packs UserSettings
.
class Session(
val id: Long,
val userSettings: UserSettings
)data class UserSettings(
val id: Long,
val name: String,
val age: Int
) {
fun ageDoubler(age: Int): Int {
return age + age
}
}
As you can see, our UserSettings
has a method also that does some *serious calculations* (not really), so we need to unit test it. As the class is quite *big* (again, I’m joking, its only for demonstration) to create every time we want to use it, we’ll create Mocks
class that will let us easier generate these entity models and put it in test scope (it would not be cool to have this class in production app, would it?).
object Mocks {
fun createUserSettings(
id: Long = 1L,
name: String = "valid_name",
age: Int = 10
): UserSettings {
return UserSettings(
id = id,
name = name,
age = age
)
}
fun createSession(
id: Long = 1L,
userSettings: UserSettings = createUserSettings()
): Session {
return Session(
id = id,
userSettings = userSettings
)
}
}
There we go, it even has default entity values. Its very easy whenever you may need any kind of models like these. So we move on to some unit testing now.
class UserSettingsTest {
@Test
fun valid() {
// Assemble
val userSettings = Mocks.createUserSettings(
id = 2L,
name = "mark",
age = 21
)
val session = Mocks.createSession(
id = 1L,
userSettings = userSettings
)
// Act
val resultAge = userSettings.ageDoubler(10)
// Assert
assertEquals(20, resultAge)
}
}
We use our Mocks
to create the models, and test some kind of behaviour.
So till now, everything is fine and its very easy to do some testing. Now lets move on to components
module.
We’ll use the same entity models we defined before and additional class to extract some data.
class CoolComponent() {
fun extractUserName(session: Session): String {
return session.userSettings.name
}
}
And we move on to do some unit testing for this method.
class CoolComponentTest {
@Test
fun valid() {
// Assemble
val coolComponent = CoolComponent()
val session = Mocks.createSession(
userSettings = Mocks.createUserSettings(
id = 2L,
name = "cool_name",
age = 16
)
)
// Act
val resultName = coolComponent.extractUserName(session)
// Assert
assertEquals("cool_name", resultName)
}
}
So, IDE does not have any problem using any of these things. We see Session
, we see UserSettings
and even Mocks
class. But the problems start whenever you try to run things.
e: …/components/CoolComponentTest.kt: (3, 21): Unresolved reference: Mocks
When running tests in components
module that reference Mocks
that is in models
module test scope, this breaks bad :(
Solution
So to solve this,
- We will be creating another module —
mock_factory
that holds only classes which holds helper factory methods to create objects
- We move mock factory class
Mocks
frommodels
module test folder tomock-factory
module main folder. That means this class can be seen in all modules that are using this - We define that our
mock-factory
module will be usingmodels
module as a dependency (we will need the scope of the objects that we will be creating). To do this, we update ourbuild.gradle
for themock-factory
module:
dependencies {
…
implementation project(‘:models’)
…
}
- We update our modules that will be using
Mocks
creation factory, so we update ourbuild.gradle
to ourmodels
andcomponents
modules. The only difference is we will be injecting class with test scope.
dependencies {
…
testImplementation project(‘:mock-factory’)
…
}
- We clean and rebuild the project…
- ..and run the tests for
models
andcomponents
modules. All the tests are running fine and we have central repository for easier mock creation factory. Yay!
So before solving this, I’ve had a couple of different approaches for this. Some included code duplication, some .jar generation and other tricks. Whenever I’ve found such an elegant solution, I knew I have to share it. So hope this will help out anyone and will make the unit tests more happy.