Automated testing is an extremely useful tool and, therefore, we recommend writing tests for all RapidSMS applications and projects. Tests provide a way to repeatedly ensure that your code functions as expected and that new code doesn’t break existing functionality.
This document outlines the tools and best practices for writing RapidSMS tests.
A RapidSMS test is written using standard Python and Django testing utilities. If you’re unfamiliar with these concepts, please take a moment to read through the following links:
Additionally, since much of RapidSMS is Django-powered, these docs will not cover testing standard Django aspects (views, models, etc.), but rather focus on the areas unique to RapidSMS itself, specifically messaging and the router.
Let’s start with an example. Say you’ve written a quiz application, QuizMe, that will send a question if you text the letter q to RapidSMS:
You: q
RapidSMS: What color is the ocean? Answer with 'q ocean <answer>'
You: q ocean red
RapidSMS: Please try again!
You: q ocean blue
RapidSMS: Correct!
Additionally, if no questions exist, the application will inform you:
You: q
RapidSMS: No questions exist.
While the application is conceptually simple, determining what and how to test can be a daunting task. To start, let’s look a few areas that we could test:
How to test these aspects is another question. Generally speaking, it’s best practice, and conceptually the easiest, to test the smallest units of your code. For example, say you have a function to test if an answer is correct:
class QuizMeApp(AppBase):
def check_answer(self, question, answer_text):
"""Return if guess is correct or not"""
guess = answer_text.lower()
answer = question.correct_answer.lower()
return guess == answer
Writing a test that uses check_answer directly will verify the correctness of that function alone. With that test written, you can write higher level tests knowing that check_answer is covered and will only fail if the logic changes inside of it.
The following sections describe the various methods and tools to use for testing your RapidSMS applications.
RapidSMS provides a suite of test harness tools. Below you’ll find a collection of django.test.TestCase extensions to make testing your RapidSMS applications easier.
The RapidTest class provides a simple test environment to analyze sent and received messages. You can inspect messages processed by the router and, if needed, see if messages were delivered to a special backend, mockbackend. Let’s take a look at a simple example:
from rapidsms.tests.harness import RapidTest
class QuizMeStackTest(RapidTest):
def test_no_questions(self):
"""Outbox should contain message explaining no questions exist"""
self.receive('q', self.lookup_connections('1112223333')[0])
self.assertEqual(self.outbound[0].text, 'No questions exist.')
In this example, we want to make sure that texting q into our application will return the proper message if no questions exist in our database. We use receive to communicate to the router and lookup_connections to create a connection object to bundle with the message. Our app will respond with a special message, No questions exist, if the database isn’t populated, so we inspect the outbound property to see if it contains the proper message text. That’s it! With just a few lines we were able to send a message through the entire routing stack and verify the functionality of our application.
Inherits from TestRouterMixin, LoginMixin, TestCase.
RapidTest provides flexible means to check application state, including the database. Here’s an example of a test that examines the database after receiving a message:
from rapidsms.tests.harness import RapidTest
from quizme.models import Question, Answer
class QuizMeGeneralTest(RapidTest):
def test_question_answer(self):
"""Outbox should contain question promt and answer should be recorded in database"""
Question.objects.create(short_name='ocean',
text="What color is the ocean?",
correct_answer='Blue')
msg = self.receive('q ocean blue', self.lookup_connections('1112223333')[0])
# user should receive "correct" response
self.assertEqual(self.outbound[0].text, 'Correct!')
# answer from this interaction should be stored in database
answer = Answer.objects.all()[0]
self.assertTrue(answer.correct)
self.assertEqual(msg.connection, answer.connection)
If you have application logic that doesn’t depend on message processing directly, you can always test it indepdently of the router API. RapidSMS applications are just Python classes, so you can construct your app inside of your test suite. For example:
from django.test import TestCase
from rapidsms.router.test import TestRouter
from quizme.app import QuizMeApp
class QuizMeLogicTest(TestCase):
def setUp(self):
# construct the app we want to test with the TestRouter
self.app = QuizMeApp(TestRouter())
def test_inquiry(self):
"""Messages with only the letter "q" are quiz messages"""
self.assertTrue(self.app.is_quiz("q"))
def test_inquiry_whitespace(self):
"""Message inquiry whitespace shouldn't matter"""
self.assertTrue(self.app.is_quiz(" q "))
def test_inquiry_skip(self):
"""Only messages starting with the letter q should be considered"""
self.assertFalse(self.app.is_quiz("quantity"))
self.assertFalse(self.app.is_quiz("quality 50"))
This example tests the logic of QuizMeApp.is_quiz, which is used to determine whether or not the text message is related to the quiz application. The app is constructed with TestRouter and tests is_quiz with various types of input.
This method is useful for testing specific, low-level components of your application. Since the routing architecture isn’t loaded, these tests will also execute very quickly.
You can write high-level integration tests for your applications by using the TestScript framework. TestScript allows you to write message scripts (akin to a movie script), similar to our example in the What To Test section above:
You: q
RapidSMS: What color is the ocean? Answer with 'q ocean <answer>'
You: q ocean blue
RapidSMS: Correct!
The main difference is the syntax:
1112223333 > q
1112223333 < What color is the ocean? Answer with 'q ocean <answer>'
1112223333 > q ocean blue
1112223333 < Correct!
The script is interpreted like so:
The entire script is parsed and executed against the RapidSMS router.
To use this functionality in your test suite, you simply need to extend from TestScript or TestScriptMixin to get access to runScript():
from rapidsms.tests.harness import TestScript
from quizme.app import QuizMeApp
from quizme.models import Question
class QuizMeScriptTest(TestScript):
apps = (QuizMeApp,)
def test_correct_script(self):
"""Test full script with correct answer"""
Question.objects.create(short_name='ocean',
text="What color is the ocean?",
correct_answer='Blue')
self.runScript("""
1112223333 > q
1112223333 < What color is the ocean? Answer with 'q ocean <answer>'
1112223333 > q ocean blue
1112223333 < Correct!
""")
This example uses runScript to execute the interaction against the RapidSMS router. apps must be defined at the class level to tell the test suite which apps the router should load. In this case, only one app was required, QuizMeApp.
This test method is particularly useful when executing high-level integration tests across multiple RapidSMS applications. However, you’re limited to the test script. If you need more fined grained access, like checking the state of the database in the middle of a script, you should use General Testing.
Inherits from TestScriptMixin, TransactionTestCase.
The scripted.TestScript class subclasses unittest.TestCase and allows you to define unit tests for your RapidSMS apps in the form of a ‘conversational’ script:
from myapp.app import App as MyApp
from rapidsms.tests.scripted import TestScript
class TestMyApp (TestScript):
apps = (MyApp,)
testRegister = """
8005551212 > register as someuser
8005551212 < Registered new user 'someuser' for 8005551212!
"""
testDirectMessage = """
8005551212 > tell anotheruser what's up??
8005550000 < someuser said "what's up??"
"""
This TestMyApp class would then work exactly as any other unittest.TestCase subclass (so you could, for example, call unittest.main()).
Inherits from TestRouterMixin.
Run a test script.
Parameters: | script (string) – A multi-line test script. See TestScriptMixin. |
---|
Full name of rapidsms.tests.harness.TestScriptMixin.
Below you’ll find a list of mixin classes to help ease unit testing. Most of these mixin classes are used by the RapidSMS test classes for convenience, but you can also use these test helpers independently if needed.
The CreateDataMixin class can be used with standard TestCase classes to make it easier to create common RapidSMS models and objects. For example:
from django.test import TestCase
from rapidsms.tests.harness import CreateDataMixin
class ExampleTest(CreateDataMixin, TestCase):
def test_my_app_function(self):
contact1 = self.create_contact()
contact2 = self.create_contact({'name': 'John Doe'})
connection = self.create_connection({'contact': contact1})
text = self.random_string()
# ...
Base test mixin class that provides helper functions to create data.
No superclasses.
Create and return RapidSMS backend object. A random name will be created if not specified in data attribute.
Parameters: | data – Optional dictionary of field name/value pairs to pass to the object’s create method. |
---|
Create and return RapidSMS connection object. A random identity and backend will be created if not specified in data attribute.
Parameters: | data – Optional dictionary of field name/value pairs to pass to the object’s create method. |
---|
Create and return RapidSMS contact object. A random name will be created if not specified in data attribute.
Parameters: | data – Optional dictionary of field name/value pairs to pass to the object’s create method. |
---|
Create and return RapidSMS IncomingMessage object.
Create and return RapidSMS OutgoingMessage object. A random template will be created if not specified in data attribute.
Parameters: | data – Optional dictionary of field name/value pairs to pass to OutgoingMessage.__init__. |
---|
Generate a random string of characters.
Parameters: |
|
---|
Generate a random string of unicode characters.
Parameters: | length – Length of generated string. |
---|
Full name for rapidsms.tests.harness.CreateDataMixin.
The CustomRouterMixin class allows you to override the RAPIDSMS_ROUTER and INSTALLED_BACKENDS settings. For example:
from django.test import TestCase
from rapidsms.tests.harness import CustomRouterMixin
class ExampleTest(CustomRouterMixin, TestCase)):
router_class = 'path.to.router'
backends = {'my-backend': {'ENGINE': 'path.to.backend'}}
def test_sample(self):
# this test will use specified router and backends
pass
Inheritable TestCase-like object that allows Router customization.
Inherits from CreateDataMixin.
Dictionary to override INSTALLED_BACKENDS during testing. Defaults to {}.
get_router() API wrapper.
List to override RAPIDSMS_HANDLERS with, or if None, leave RAPIDSMS_HANDLERS alone
loopup_connections() API wrapper.
A wrapper around the receive API. See Receiving Messages.
String to override RAPIDSMS_ROUTER during testing. Defaults to 'rapidsms.router.blocking.BlockingRouter'.
A wrapper around the send API. See Sending Messages.
Full name for rapidsms.tests.harness.CustomRouterMixin.
TestRouterMixin extends CustomRouterMixin and arranges for tests to use the rapidsms.router.test.TestRouter.
Test extension that uses TestRouter
Inherits from CustomRouterMixin.
A list of app classes to load, rather than INSTALLED_APPS, when the router is initialized.
Manually empty the outbox of mockbackend.
If disable_phases is True, messages will not be processed through the router phases. This is useful if you’re not interested in testing application logic. For example, backends may use this flag to ensure messages are sent to the router, but don’t want the message to be processed.
The list of message objects received by the router.
A wrapper around the lookup_connections API. See Connection Lookup.
The list of message objects sent by the router.
The list of message objects sent to mockbackend.
Full name for rapidsms.tests.harness.TestRouterMixin.
The TestRouter can be used in tests. It saves all messages for later inspection by the test.
Router that saves inbound/outbound messages for future inspection.
Inherits from BlockingRouter.
List of all the inbound messages
List of all the outbound messages
Save all inbound messages locally for test inspection
Save all outbound messages locally for test inspection
The DatabaseBackendMixin helps tests to use the DatabaseBackend.
Arrange for test to use the DatabaseBackend, and add a .sent_messages attribute that will have the list of all messages sent.
Inherits from CustomRouterMixin.
lookup_connections wrapper to use mockbackend by default
Messages passed to backend.
Helpers for creating users and logging in
If not already set, creates self.username and self.password, otherwise uses the existing values. If there’s not already a user with that username, creates one. Sets self.user to that user. Logs the user in.
Full name for rapidsms.tests.harness.LoginMixin.
Some of these classes inherit from:
which is the full name for django.test.TestCase.