When a function or class requires access to another function or class in order to do its work, we say that the latter is a "dependency" of the former. For example, the function below depends on the print function.
def sendMessage(message):
print('Message: ' + message)
This might seem like an unusual way to think about our code, why does it matter what we depend on? Let's say you wanted to test this function. You could read from stdout to make sure your message got printed, but that's clunky (and if this were a network request or a database query perhaps quite unreasonable). Instead, we can use dependency injection to "inject" a "print" function.
def sendMessage(message, printFunc):
printFunc('Message: ' + message)
Now, in our test we can give it a function that stores whatever arguments it is called with so that we can verify that they are correct without having to mess around with stdout.
class MessageTrap:
def __init__():
self.messages = []
def print(self, message):
self.messages.add(message)
def testSendMessage():
trap = MessageTrap()
sendMessage('hello there', trap.print)
# verify trap.messages
If we really wanted to get fancy we could give printFunc a default value, perhaps setting it to the standard print function by default.
Further reading:
We've probably all heard that functions are black boxes that take inputs and produce outputs. And you've probably seen a diagram like the one below before. But what does it mean for a function to be "pure" and how do pure functions help us test our code?
A pure function is one that takes inputs and produces outputs, as illustrated above and, furthermore, has no "side effects" and no dependencies on any state outside of the function inputs. A side effect is any modification of the "world" outside the function itself. For example, the function below prints to the screen, which is a side effect:
def add(a, b):
c = a + b
print(c)
return c
So even though this function also returns a value, it is not a "pure" function. Another example of a side effect is modifying external state, such as the state of a class instance:
class Greeter:
def __init__(self):
self.greeting = 'hello'
def setGreeting(self, greeting):
self.greeting = greeting
In this case, the setGreeting function doesn't even return a value, it just modifies state outside of the function itself.
There's nothing wrong with impure functions, in fact without them we'd never be able to see the results of our computations. But they tend to be more difficult to test than pure functions. The reason for this is that we have to check more places to find out whether the function did what it was supposed to do. In the first example above, we can easily assert that the value returned from the function is the sum of its two arguments, but how would we verify that it printed that sum to the console?
In the second example above, once we've called the setGreeting function we have to check the greeting attribute on the class to find out if it worked or not. If setGreeting added to the string, or transformed it in some other way, we'd also need to make sure that we started out with the correct value in the greeting attribute before running the test. Consider the following class:
class Adder:
def __init__(self):
self.sum = 0
def add(self, value):
self.sum = self.sum + value
Note that the add method is dependent on the current state of the sum attribute. If we change sum before we call add we will get a different result for a given argument (such as 5). In order to test this reliably we need to be careful not to allow the sum attribute to be modified before we get our hands on it, or we need to remember to reset it to our preferred start value. If we used a pure function instead our test would be much simpler to think about, and probably a bit simpler to write as well:
def adder(sum, addend):
return sum + addend
These are pretty silly examples, but they do illustrate the differences between pure and impure functions.
Further reading:
Anyone who has been exposed to Java should have some familiarity with interfaces. Put simply, an interface is a description of the methods that must be available on a class instance that implements the interface. I might help to think about an interface as a description of the "shape" of an object.
public interface Sender {
void send(String message);
}
But how do interfaces help us with testing? Largely by letting us create simple test doubles more easily. This is going to look very similar to dependency injection, and that's because it is. Interfaces are one way to enable dependency injection. First, let's look at a class that we might use to talk to a chat service, like Slack or Discourse.
public class Client {
void send(String message) {
// verify server connection
// send message
// check for errors
}
}
Looks pretty reasonable. Now let's say we have bot that interacts with our chat client in one way or another (maybe it tells a joke whenever someone says "tell me a joke" in chat).
public class JokeBot {
private Client client;
public JokeBot() {
this.client = new Client();
}
public void tellJoke() {
String nextJoke = "i just flew in, boy are my arms tired";
client.send(nextJoke);
}
}
This looks great. But now how do we test our JokeBot class? We can't just create a new instance and use it, because it will create a client, which will attempt to connect to a chat server (presumably). We can fix this problem using dependency injection.
public class JokeBot {
private Client client;
public JokeBot(Client client) {
this.client = client;
}
public void tellJoke() {
String nextJoke = "i just flew in, boy are my arms tired";
client.send(nextJoke);
}
}
OK, so now the JokeBot constructor doesn't attempt to create a new Client, but we still have to give it a Client to use, and if we try to create one we'll end up in the same place we were a minute ago because the Client will try to talk to a chat server. Enter interfaces...
public interface Client {
void send(String message);
}
We change Client from a concrete class to an interface. Now we can rename our previous Client class to be more specific and implement Client:
public class NetworkClient implements Client {
void send(String message) {
// ...
}
}
Finally, we can define another Client implementation that doesn't try to connect to a server but instead simply tracks the messages it has been asked to send (just like in the dependency injection example):
public class TestClient implements Client {
String lastMessage;
void send(String message) {
lastMessage = message;
}
}
Now we can use TestClient for our JokeBot tests, which allows us to make sure that JokeBot sends the jokes we expect it to send, when we expect it to send them, but without having to connect to a chat server or use a real chat room.
Exercise: there is another obvious point at which we might use an interface and dependency injection in the JokeBot class, can you identify it?
Further reading:
This principle basically states that a given module, or other unit of code, should do one thing in the context of the overall software package. For example, if you have a class called SpellChecker, it should do one thing (probably check spelling). It shouldn't participate in the layout of a document, or even perhaps grammar checking. It probably also shouldn't be responsible for creating the interface elements shown to the user during the spell checking process. This idea is sometimes also called "cohesion".
This principle applies to software testing because a module that has a single responsibility will often be easier to test, and easier to change in the future. This is partially because dependencies are minimized, and partially because simpler, more cohesive modules are less likely to change frequently. Once they work they tend to continue to work, which is really our ultimate goal.
Further reading:
A factory is a place where similar products are produced in mass quantities. For example, a car factory might produce one model of car, but each car produced may have different features. Some might have leather seats, others cloth. They may come in different colors. Some of their engines might have six cylinders, others four. Or, a particular factory might be able to produce both car model X and car model Y. In this case, the factory might switch between the two based on the prevailing situation (demand, corporate strategy, and so on).
In software, a "factory" is similar, though it produces instances of a class or other data structure instead of physical objects. Let's look at an example.
public abstract class Character {
public void move(int direction);
public void speak();
public static Character makeCharacter(int level) {
if (level % 2 == 0) {
return new Mario();
} else {
return new Luigi();
}
}
}
public class Mario extends Character { ... }
public class Luigi extends Character { ... }
In this case, we want to use Mario for even-numbered levels, and Luigi for odd-numbered levels (for whatever reason). So depending on which level we specify, we get back either a Mario or a Luigi transparently from the factory. Note that this probably seems similar to Dependency Injection and, in fact, in many cases we treat a factory as a dependency, then we can choose which factory to provide to a class or method based on some other criteria. For example:
public abstract class Character {
// ...
public static Character makeEasyCharacter() {
return new Mario();
}
public static Character makeDifficultCharacter() {
return new Luigi();
}
}
In this case, we want to supply a different character depending on the difficulty level chosen by the player. But our game logic doesn't need to care about the difficulty, or which character is assigned to which difficulty level. Instead, we simply provide the game logic with the correct factory (through injection) and everything works as it should.
This pattern is often used to provide a different implementation of something like a database connection depending on the environment in which the program is running (including a testing environment).
Cyclomatic complexity is basically a measure of how many "paths" exist through a section of code. This has implications for testing because code with greater cyclomatic complexity (more paths) can be more difficult to test thoroughly. Take, for example, the following code:
def max(a, b):
m = a
if b > a:
m = b
return m
We could convert this to a directed graph easily enough:
There are two unique paths through this graph, so when we test this function we need to be sure to test both paths. In this case, as long as we do a good job choosing test values we'll be just fine, but more complicated functions can be trickier.
One way to reduce this burden is to try to reduce the cyclomatic complexity of our code. For example, it is generally considered bad practice to have deeply-nested conditional statements (if-else inside an if-else inside an if-else, and so on). Another trick some people use to make these paths easier to think about is the early exit, returning from a function immediately if there is no work to be done or if the result can be computed trivially. This doesn't necessarily make our code less complex mathematically speaking, but it makes the complexity easier to deal with for our puny human brains.