Engineering for All: Faking

If you believe it, they believe it.

267th Ferengi Rule of Acquisition

All war is deception

Sun Tzu

The dangers of testing

Let's face it testing software can be hard. Even with the best intentions, our tests can easily break. This phenomenon is called brittle unit tests.

Brittle Unit tests

Unit testing has a very bad reputation, and I believe one issue so many very talented developers have with unit testing is that it's very easy to write brittle, and tightly coupled tests.

My current philosophy on testing is to focus on requirements and test what the code accomplishes not how it gets there. What I believe comes from this approach are tests on “public” APIs, that avoid, if possible, private implementation. This will help reduce tightly coupled tests, but your tests will still be tightly coupled to external resources. We can then use stubs and mocks to test our code in isolation.

Types of fakes

When running your tests you typically run into two issues with resources.

  1. My test doesn’t test any interaction with the resource, but my code under test requires that I talk to the resource in order to run.
  2. My test is looking at how my code under test is interacting with the resource and I want to make sure the code under test does something specific.

Type 1 requires a stub, which is nothing more than code that mimics a resource but doesn’t keep track of that interaction.

Type 2 requires a mock, and in this case, we want to see how our unit of code interacted with the resource.

Manually Faking

There are two schools of thought when faking, manually writing the code, or use a mocking framework.

I will first show how to manually fake in Go, then how to use 2 Mocking packages in Python.

Go:

In the below example, you see three cases where we are mocking.

Our first test is stubbing the sleep method call and checking the contents being printed.

In the second test, we are stubbing the print method and checking the amount of the times we call the sleep method.

In Go we have two very useful tools for faking pointers and interfaces. Pointers allow us to take advantage of pass by reference and view our fake interactions. Interfaces allow us to create a seem in our code and replace external resources.

# hello.go
package main
import (
"fmt"
"io"
"os"
)
type Sleeper interface {
Sleep()
}

func Hello(writer io.Writer, sleeper Sleeper) {
sleeper.Sleep()
sleeper.Sleep()
sleeper.Sleep()
sleeper.Sleep()
fmt.Fprintf(writer,"Hello World")
}
func main() {
Hello(os.Stdout)
}
# hello_test.go
package main
import (
"bytes"
"fmt"
"testing"
)
//testing for a specific string printed
func TestHelloString(t *testing.T) {
buffer := bytes.Buffer{} // our mock
spySleeper := &SpySleeper{} // our stub
Hello(&buffer, spySleeper)
got := buffer.String()
want := "Hello World"
if got != want {
t.Errorf("got %q want %q", got, want)
}
}
//tests the amount of times sleep is called
func TestHelloSleepCount(t *testing.T) {
buffer := bytes.Buffer{} //our stub
spySleeper := &SpySleeper{} // our mock
Hello(&buffer, spySleeper)
want := 4
got := spySleeper.Calls
if got != want {
t.Errorf("got %q want %q", got, want)
}

}
//used to store the amount of sleep calls
type SpySleeper struct {
Calls int
}
// used to incrament the sleep calls count
func (s *SpySleeper) Sleep() {
s.Calls++
}

The downsides of manually faking are

  • it requires us to understand how the language works (not really a bad thing!),
  • it adds complexity to your code.

Python using unittest.mock :

Our Unit under test:

We see here that we have a function that waits 4 times, each time 10 seconds, and then prints (“Hello World”).

# hello.pyimport timedef hello_world():
time.sleep(10)
time.sleep(10)
time.sleep(10)
time.sleep(10)
print("Hello World")

Testing:

Mocking imports:

In this case, we are using the patch feature to create a decorator that looks at our module under test, and monkey patches the sleep function in the time package.

What is useful in this case is that we can fake the imported package in the module under test and just run a test.

# test_hello.pyfrom unittest.mock import patch
import hello
from time import sleep
# Stub
@patch("hello.time.sleep")
def test_module_stub(mock_time):
hello.hello_world()
assert 1 == 1

# Mock
@patch("modulea.time.sleep")
def hello_world(mock_time):
hello.hello_world()
assert mock_time.call_count == 4

Manually monkey patching:

As a side note, we can also manually monkey patch in Python by overwriting sys.modules[‘time’] with a Mock object although I feel using patch is a cleaner approach.

# test_hello.pyfrom unittest.mock import Mock
import sys

sys.modules['time'] = Mock()
import hello

def test_modulea():
hello.hello_world()
assert 1 == 1

Using Abstraction:

Although monkey patching can meet our requirements, I prefer to decouple our code. We will apply the idea of separation of concerns and isolate the interaction with the time package.

Refactoring:

In the code below we are using a class to wrap the sleep function.

# hello.py
import time
class BreakManager():
def take_break(self):
time.sleep(10)
def hello_world(break_manager):
break_manager.take_break()
break_manager.take_break()
break_manager.take_break()
break_manager.take_break()
print("Hello World")

We can now refactor our tests to hand a mock object instead of the real BreakManager instance.

import hello
from unittest.mock import Mock

# stub
def test_module_stub():
break_manager = Mock()
hello.hello_world(break_manager)
assert 1 == 1

# mock
def test_modulea_sleep():
break_manager = Mock()
hello.hello_world(break_manager)
assert break_manager.hello_world.call_count == 5

I understand this isn’t always possible especially when working with other people’s code, in cases where abstractions are not possible I would fall back to the patch pattern.

Mocking AWS resources with Python:

When writing unit tests with Python there is a library that makes mocking AWS resources very easy called moto. The basic idea behind moto is that you create a mini “AWS” and then you manipulate it as needed by your test.

In the example below, we see that we are able to test getting a file from AWS S3 using moto, first by “putting” the file in moto S3.

###########  df.py
import boto3


def put_data_frame(bucket, key, local_file_name):
s3 = boto3.client(
"s3",
region_name="us-east-1",
aws_access_key_id="abcd",
aws_secret_access_key="efgh")
return s3.put_object(Bucket=bucket, Key=key, Body=local_file_name)


def get_data_frame(bucket, key, local_file_name):
s3 = boto3.client(
"s3",
region_name="us-east-1",
aws_access_key_id="abcd",
aws_secret_access_key="efgh")
return s3.download_file(bucket, key, local_file_name)
########### test_df.py
import boto3
from moto import mock_s3
import df


@mock_s3
def test_get_data_frame():
bucket_name = "dataframe-bucket"
conn = boto3.resource('s3', region_name='us-east-1')
conn.create_bucket(Bucket=bucket_name)
model_instance = df
model_instance.put_data_frame(bucket_name, "report.csv", "reports")
model_instance.get_data_frame(bucket_name, "report.csv", "reports")

TLDR

Separate your integration tests from your unit tests, and use fakes to make your tests easy and reliable. Avoid tightly coupled tests, after all, tests are code too. When writing tests, focus on what the code does not how it gets there.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store