### End-to-End Testing - A Code Example

Prior posts in this series:

Abstract concepts are always easier to learn with concrete examples. This is the first code-heavy post in the series, and it is intended to illustrate the mechanics of the concept, not yet how it is applied to the problem domain. For this reason, I use the example of Fibonacci Numbers, with a naive implementation - the business logic is not the emphasis here, but how we test them on multiple levels.

I chose Django to write the code, in because of its lovely built in end-to-end testing support, but knowing that not everyone is a Django developer (yet!), I chose not to use any of the more idiomatic Django class based views, since the purpose of this post is not to teach idiomatic Django.

Enough of the disclaimers, on to the code!

# The Specs for the App

```class FibonacciCalculatorTests:

def test_cannot_calculate_sequence_elements_less_than_one(self):
self.assert_cannot_calculate(n=-1)
self.assert_cannot_calculate(n=0)
self.assert_can_calculate(1)

def test_currently_we_cannot_calculate_numbers_greater_than_10(self):
self.assert_can_calculate(10)
self.assert_cannot_calculate(11)
self.assert_cannot_calculate(7895)

def test_can_calculate_the_first_ten_fibonacci_numbers(self):
self.assertEquals(1, self.get_fibonacci(1))
self.assertEquals(1, self.get_fibonacci(2))
self.assertEquals(2, self.get_fibonacci(3))
self.assertEquals(3, self.get_fibonacci(4))
self.assertEquals(5, self.get_fibonacci(5))
self.assertEquals(8, self.get_fibonacci(6))
self.assertEquals(13, self.get_fibonacci(7))
self.assertEquals(21, self.get_fibonacci(8))
self.assertEquals(34, self.get_fibonacci(9))
self.assertEquals(55, self.get_fibonacci(10))

def test_can_only_calculate_fibonacci_for_integers(self):
self.assert_cannot_calculate(3.14)
self.assert_cannot_calculate('not a number')
self.assert_cannot_calculate(None)

def assert_can_calculate(self, n):
result = self.get_fibonacci(n)
self.assertIsNotNone(result)
```

We'll get to the implementation of assert_cannot_calculate next.

Let's add the actual test implementation

```from django.test import TestCase
from simple_fibonacci import calculator, urls

class InMemory(FibonacciCalculatorTests, TestCase):

def assert_cannot_calculate(self, n):
with self.assertRaises(ValueError):
self.get_fibonacci(n)

def get_fibonacci(self, n):
return calculator.fibonacci(n)
```

Followed by the calculator fibonacci logic

```def fibonacci(n):
# don't do this in production
# use https://en.wikipedia.org/wiki/Fibonacci_number#Closed-form_expression
if n < 1:
raise ValueError('value (%r) too low' % n)
if n > 10:
raise ValueError('value (%r) too high' % n)
if n in (1, 2):
return 1
return fibonacci(n - 1) + fibonacci(n - 2)
```

# This is a webservice, let's test the JSON API!

You can probably guess, this is where we add the second implementation for the FibonacciCaulcatorTests:

```import simple_fibonacci
from django.test.client import Client
from django.core.urlresolvers import reverse
import json

class JsonHttpResponse(FibonacciCalculatorTests, TestCase):

urls = simple_fibonacci.urls

def assert_cannot_calculate(self, n):
data = self.get_fibonacci_parsed_json_response(n)
self.assertFalse('result' in data, data)
self.assertEquals('ERROR', data['status'])

def get_fibonacci(self, n):
data = self.get_fibonacci_parsed_json_response(n)
self.assertFalse('error' in data, data)
self.assertEquals('OK', data['status'], data)
return data['result']

def get_fibonacci_parsed_json_response(self, n):
client = Client()
url = reverse('fibonacci')
response = client.post(url, {'n': n}, HTTP_ACCEPT='application/json')
allowed_keys = set(['status', 'n', 'error', 'result'])
data_keys = set(data.keys())
self.assertEquals(set([]), data_keys - allowed_keys)
self.assertEquals(unicode(n), data['n'])
return data
```

As you can see, on top of the assertions in the base class, I've added a few additional assertions, for the api contract (invariants) - this api shouldn't return extra fields, or if it does, I should be notified and then decided whether that's a bug that needs correction or a feature, in which case I'll adjust the allowed_keys variable

And here is the implementation:

```from django.views.generic import View
import json
from django.http import HttpResponse
from simple_fibonacci import calculator

class FibonacciView(View):

def post(self, request):
n = request.POST['n']
error = None
result = None
try:
result = calculator.fibonacci(int(n))
except ValueError as e:
error = unicode(e)
content, content_type = \
self.get_response_content_and_type_function(request)(
n, error, result
)
return self.to_response(content, content_type)

def get_response_content_and_type_function(self, request):
return self.json_response

def json_response(self, n, error, result):
response_data = {'n': n}
if error is not None:
response_data['status'] = 'ERROR'
else:
response_data['status'] = 'OK'
response_data['result'] = result
return (json.dumps(response_data), 'application/json')

def to_response(self, content, content_type):
response = HttpResponse(content)
response['Content-Type'] = content_type
return response
```

# Let's expose this feature to normal user as HTML!

The order of the events is sometimes the other way around, but sooner or later the point comes when the same functionality must be exposed through a different channel - whether it's a simplified/more efficient power user interface for your internal support people as opposed to your first time customers through the public web shop, or adding a mobile app for your web service, it will happen. And it's nice to know we can execute the same set of tests against all implementations.

Let's update the tests first:

```import re

class HtmlUserFriendlyHttpResponse(FibonacciCalculatorTests, TestCase):

urls = urls

def assert_cannot_calculate(self, n):
response = self.get_fibonacci_response(n)
self.assertContains(response=response, text='<p class="error">')
self.assertNotContains(response=response, text='<p class="success">')

def get_fibonacci(self, n):
response = self.get_fibonacci_response(n)
self.assertContains(response=response, text='<p class="success">')
self.assertNotContains(response=response, text='<p class="error">')
return int(
re.search(
r'<span id="result">(\d+)</span>', response.content
) .groups())

def get_fibonacci_response(self, n):
client = Client()
url = reverse('fibonacci')
client.get(url, HTTP_ACCEPT='text/html')  # as the user, we first load the page
post_response = client.post(
url, {'n': n}, HTTP_ACCEPT='text/html')
self.assertEquals('text/html', post_response['Content-Type'])
return post_response
```

And update our implementation:

```from django.views.generic import View
import json
from django.http import HttpResponse
from simple_fibonacci import calculator

class FibonacciView(View):

def post(self, request):
n = request.POST['n']
error = None
result = None
try:
result = calculator.fibonacci(int(n))
except ValueError as e:
error = unicode(e)
content, content_type = \
self.get_response_content_and_type_function(request)(
n, error, result
)
return self.to_response(content, content_type)

def get_response_content_and_type_function(self, request):
return self.json_response

def json_response(self, n, error, result):
response_data = {'n': n}
if error is not None:
response_data['status'] = 'ERROR'
else:
response_data['status'] = 'OK'
response_data['result'] = result
return (json.dumps(response_data), 'application/json')

def to_response(self, content, content_type):
response = HttpResponse(content)
response['Content-Type'] = content_type
return response
```

That's the concept. The series will continue with me developing an app, and sharing relevant snippets and lessons learned from that project. Stay tuned!

What do you think? I would love if you would leave a comment - drop me an email at hello@zsoldosp.eu, tell me on Twitter!

Posted on in by