Testability Tip for Swift Developers – Parameterize and Push

In a previous “Testability Tip for Swift Developers”, I discussed the principle of observability. “If it’s observable, it’s testable” was the primary conclusion of the article, and I pointed toward using the public access control modifier for parts of your app that you intend to test.

In this edition, I’d like to introduce a new principle that I try to adhere to when I’m unit testing, namely, “If it’s controllable, it’s testable”. Here’s what I mean by “controllable”…

Controllability

In Testing is to Software as Experiment is to Science, I analyzed how testing software mirrors scientific experimentation. Good scientific experiments are controllable. That is, they are set up such that everything stays as constant as possible except the thing you’re poking at.

Changing multiple things at a time in an experiment clouds the ability to verify that [tweak x] produced [y outcome]. So a scientist will do his/her best to control the environment by holding as many variables constant as possible, so that he/she can make accurate conclusions about the outcome.

The same goes for testing software. If I’m going to automate a test, I want to set up my “experiment” such that I control as much of the system as possible so that I can set up valid expectations and verify results coming from the system under test. Note that I’m using the term “system” in a very broad sense – it could be referring to an entire app, a single “object”, or a function.

Parameterize and push

So where does “parameterize and push” come into play?

Parameterizing sets us up with the ability to provide inputs into the system we’re testing. Anytime you have an input, you have the ability to supply a value of your choosing.

Serving as inputs to the system, you can view parameters as entry points for controls. They’re “controls” because we decide what those values should be before we pass them off as arguments to those parameters. So long as the system we’re testing only gets the data it uses from its inputs (its parameters), we can be guaranteed predictable, controlled outputs on the other end.

Forms of parameterization

Three forms of parameterization are common:

  1. At the instance level through initializers
  2. At the instance level through property setters
  3. At the function level through function parameters

Using an initializer, or a public variable property, or by adding parameters to your functions and using only those parameters for the function’s computation and output production, you give yourself the ability to control the system in various ways that are appropriate on a test-by-test basis.

Usefulness and examples of parameterization

Setting up your instance definitions to use a set of inputs from the very start through initialization gives you the ability to provide real “production-ready” values in your app, but fake “test-customized” values for testing. Creating fake objects for testing is outside the scope of this article, but providing public initializers with parameters is a really great way to set yourself up for being able to test that particular instance.

 1// Prefer
 2public class DatabaseCommunicator {
 3    let database: Database
 4    
 5    public init (database: Database) {
 6        self.database = database
 7        // able to supply a controlled input via parameter, such as supplying a 
 8        // customized "fake" database to use for testing but still supply a "real" database in real life
 9    }
10}
11
12// over
13public class DatabaseCommunicator {
14    let database = Database()
15    // stuck with talking to a real database...
16}

Another viable option is to provide public variable properties that can be set after the instance is initialized. This is a little more round-about, but I would still call it a form of “parameterization” because the strategy still provides you with the same control point that an initializer with parameters does.

 1// prefer
 2public class DatabaseCommunicator {
 3    public var database: Database
 4    // able to supply a controlled input via property setter, such as supplying a 
 5    // customized "fake" database to use for testing but still supply a "real" database in real life
 6    public init() { self.database = Database()}
 7}
 8
 9// over
10public class DatabaseCommunicator {
11    let database = Database()
12    // stuck with talking to a real database
13}

At the function level, the usefulness of parameters is that you can supply inputs and examine outputs with ease. If you pull in data from the encapsulating instance inside the function body, say by referencing self.somePropertyValue, you’ve got a bit more setup to do to be able to accurately verify results. somePropertyValue needs to actually have a value before the function will produce accurate results. If you’d opted to simply define parameters for everything the function needs in order to produce its output, you can test that function in isolation far more easily and be guaranteed that your results are correct and accurate.

 1// prefer
 2func getNameFromDatabase(database: Database) -> String {
 3    return database.getName()
 4    // able to supply a controlled input via parameter, such as supplying a 
 5    // customized "fake" database to use for testing but still supply a "real" database in real life
 6   
 7}
 8
 9// over
10func getNameFromDatabase() -> String {
11    let database = Database()
12    // stuck with talking to a real database
13    return database.getName()
14}
15
16// and over
17func getNameFromDatabase() -> String {
18    return self.database.getName()
19    // requires additional setting of the database property on 'self'
20    // before you're able to get results from this function
21}

Parameterization or “Dependency Injection”?

Yes.

What I’m calling “parameterization” is really just “dependency injection”. But the term “dependency injection” can sound really daunting, while we’re used to working with parameters. I intend for the meaning of each term for the purpose of this article to be equivalent.

Wrapping up

Parameterizing, your instance definitions and functions provides you an immense amount of leverage when it comes to controlling your system under test. I encourage you to try this out and do your best to shift to a more parameterized approach to writing your code for improved testability. Remember, “Controllable is testable”!

comments powered by Disqus