While writing software, we are faced with lots and lots of interfaces.
The public interface for some tool or module or package or whatever is usually referred to as the API, the CLI, or simply the user interface.
In this post, when considering an interface, I am specifically referring to APIs and CLIs.
Sometimes, we find a software component that does exactly what we want, but the interface is not what we want.
That’s where software interface adapters come to the rescue.
Adapters are helpful in many stages of the design and development of just about anything.
Software interface adapters are perfect for functional testing.
On this site, I use a flavor of TDD that focuses on functional testing.
I’ve usually got a couple of goals:
- I want to test the public interface: either API, CLI, or both.
- I want the tests tat make up my regression test suite to be as easy to write as possible, so that it’s simple to add new tests as needed.
Often, these goals are at odds. The API/CLI may just not be convenient for easy testing.
That’s an ideal place to develop an interface adapter.
In this post, I’ll describe how interface adapters can be easily specified and implemented in python.
As a working example, I’ll describe the interface adapter that I need for testing markdown.py.
Functional testing at the public interface
I often use TDD when designing and developing really any piece of software.
I’ll end up writing a lot of tests in the course of development.
However, the tests that really, really, need to be solid are those that test the publicly exposed interface and functionality.
I want suite of tests that fully check the public interface and all behaviors and functionality.
Therefore, I’m obviously going to have those tests use the external/public interface.
Designing for test
During design and development, it may become obvious that public interface is a pain to test, either because it is cumbersome, or because it’s slow.
One solution to this is to add an easier to use (and/or faster) API to the specification.
This is completely valid and quite a good practice.
The users of your software may find the extended interface handy as well.
However, you still need to test the complete interface, and convince yourself through tests that the interface(s) added for testing purposes are sufficient.
When going down this route, I always add some tests that use the inconvenient interface and compare the results with the convenient interface.
In the case of markdown.py, I’m still on the fence with this.
I probably will extend the specification to include some design for test functions.
However, I want to keep the test suite blind to these additions.
I can do this if I keep the use of the additional DFT functions in the adapter.
CLI for markdown.py
I’ve defined two interaction models for markdown.py:
- Pass in some markdown text through stdin:
cat somefile.mkd | python markdown.py > somefile.html
- Pass in a file name as a parameter:
python markdown.py somefile.mkd > somefile.html
Now, I didn’t just pull these use models out of a hat.
These are the ways I’ve used markdown.pl, and pandoc.
So, when I’m done, I should be able to replace python markdown.py
with perl markdown.pl
in my test suite and get the same results.
And the same for pandoc as well.
That’s also the way I’m usually going to use markdown.py.
The API I want for easy testing of markdown.py
The lack of python API is kind of annoying as far as testing is concerned.
To test the interface from a python test method, I’m going to have to use something like subprocess.Popen to start another python process to run my text through markdown.py.
This isn’t difficult, of course. It just makes my test code ugly if I’ve got it all over the place.
What I want is to write some code like this:
import markdown
def test_some_part_of_markdown():
the_input = '**this is probably bold**'
expected_output = '<p><strong>this is probably bold</strong></p>'
actual_output = markdown.run_markdown(the_input)
assert actual_output == expected_output
But, I can’t do that, since markdown.py doesn’t have a run_markdown()
method.
I know I’m going to have a bunch of different strings that I want to run through my markdown.py script, checking that the output is correct.
I want to make that code as easy to write as possible.
Adapters to the rescue
I know you saw this coming.
I’ve defined the interface I have and the interface I want.
The logical next step is to implement an interface adapter to bridge the two.
Software CLI/API interface adapter for markdown.py
The code below is the adapter I’m going to use for testing markdown.py.
I usually abide by YAGNI during development.
However, in this case, I know I’m going to really want three methods.
- a simple run_markdown() method that just does the conversion.
- a specific run_markdown_pipe() method to force the stdin pipe interaction.
- a specific run_markdown_file() method to force the filename as parameter interaction.
markdown_adapter.py:
"""
Software API adapter for markdown.py
This module provides a function based API to markdown.py
since markdown.py only provides a CLI.
"""
from subprocess import Popen, PIPE, STDOUT
from tempfile import NamedTemporaryFile
import os
def run_markdown(input_text):
"""
The default method when we don't care which method to use.
"""
return run_markdown_pipe(input_text)
def run_markdown_pipe(input_text):
"""
Simulate: echo 'some input' | python markdown.py
"""
pipe = Popen(['python', 'markdown.py'],
stdout=PIPE, stdin=PIPE, stderr=STDOUT)
output = pipe.communicate(input=input_text)[0]
return output
def run_markdown_file(input_text):
"""
Simulate: python markdown.py fileName
"""
temp_file = NamedTemporaryFile(delete=False)
temp_file.write(input_text)
temp_file.close()
pipe = Popen(['python', 'markdown.py', temp_file.name],
stdout=PIPE, stderr=STDOUT)
output = pipe.communicate()[0]
os.unlink(temp_file.name)
return output
Using the run_markdown.py interface adapter
Here’s some code I put together to make sure my adapter worked how I wanted it to.
You’ll probably notice that I’ve already discovered that testing strings that are passed around through pipes and such can cause unintended line ending differences. That’s why there are some rstrip calls in the comparisons.
However, since the syntax for both markdown and html treat carriage returns and line feeds as whitespace, I believe I can ignore the end of the line stuff.
I may be wrong, of course. Feel free to correct me.
Actually, please do correct me if this is a bozo assumption.
try_run_markdown.py:
"""
An example script showing how to use the api adapter
"""
from markdown_adapter import run_markdown, run_markdown_pipe, run_markdown_file
SAMPLE_INPUT = [
'apple',
'**bold**',
'*emphasis*',
'# H1 header',
'## H2 header',
'''
this
has
multiple lines''']
for s in SAMPLE_INPUT:
out = run_markdown(s)
out_pipe = run_markdown_pipe(s)
out_file = run_markdown_file(s)
print '-' * 40
print '-- this --'
print s
if s.rstrip() == out.rstrip():
print '-- returns (same as input) --'
else:
print '-- returns --'
print out,
if out != out_pipe:
print '-- run_markdown_pipe() returns something different --'
print out_pipe,
if out != out_file:
print '-- run_markdown_file() returns something different --'
print out_file,
It still doesn’t do anything?
Yep. That’s right. My implementation of still doesn’t do anything interesting.
It’s still just a stub.
But that’s enough to start writing tests, especially with my adapter implemented.
In case you missed it …
Some other posts that are directly relevant to this are listed here:
Next, some actual tests
The next step in this project is to start some actual tests.
I’m going to start with doctest, then move on to unittest, nose, and pytest.
No promises as to when something actually useful comes out of markdown.py