Testing Your Twilio Applications in Python with pytest
Time to read: 9 minutes
Many applications have to connect to different services such as document managers, building tools, vendor APIs, and so on. A few services offer sandboxes or staging environments in order to allow testing, but usually this is some kind of infrastructure that we cannot control. What if the vendor staging server goes down? Also, tests that require connecting with external services are slow and often expensive.
In this blog post we will talk about strategies to test your Python code when using REST APIs. That’s right, APIs in general. With the techniques learned here you can test code that includes connections to third-party services including Twilio APIs.
We are going to go on an adventure based on my current journey: learning German. We’ll build a simple SMS bot that sends a new German word every day using Twilio’s Programmable SMS API and then we’ll build tests for it. Even though this is a Python tutorial you may be able to apply the concepts in your preferred programming language.
Tutorial requirements
To follow this tutorial you need the following components:
- Python 3.6 or newer. If your operating system does not provide a Python interpreter, you can go to python.org to download an installer.
- A Twilio account. If you are new to Twilio create a free account now. If you use this link to sign up, you will receive $10 in credit when you upgrade to a paid account.
Create a Python virtual environment
Following Python best practices, we are going to make a separate directory for our Learning German via SMS project, and inside it we are going to create a virtual environment. We then are going to install the Python packages that we need for our project on it.
If you are using a Unix or Mac OS system, open a terminal and enter the following commands to do the tasks described above:
For those of you following the tutorial on Windows, enter the following commands in a command prompt window:
The last command uses pip
, the Python package installer, to install the packages that we are going to use in this project, which are:
- The Twilio Python Helper library, to work with the Twilio APIs
- Pytest, the most popular test framework for Python that makes building simple and scalable tests easy
For your reference, at the time this tutorial was released these were the versions of the above packages and their dependencies tested:
Get your tokens from the Twilio Console
In order to connect with Twilio’s SMS API we need two pieces of information: the Account SID and the Auth Token. Both are needed to prove that you are you and keep your application secure. You just need to access your Twilio Console and leave this tab open (see the image below). We’ll be right back to it.
Buy a Twilio phone number
In order to use the SMS service, you need a phone number to be used as your sender. You can go to the Phone Numbers page from your dashboard or from the “#” button on the left (see the images below):
Twilio’s dashboard page
Phone numbers page:
Before we dive into testing, let’s talk a bit about our project: Learning German via SMS!
Creating a SMS service
It’s time to learn German, oder?
Building a words database
After reading one of these books about learning new languages, I found that a nice way of building vocabulary in a foreign language is to memorize the top 100, 500, and then the top 1000 most frequent words. So I built this JSON list with the top 1000 most frequent German words. The service we’re about to build will send me a SMS twice a day with a random word. This way, I’ll always be learning a new word.
You can create a JSON file with anything you want to memorize. It’s ok, it doesn’t have to be German. :) I got my list of most frequent German words from the Language Daily website. Here is how a word looks like in my JSON file:
After deciding what you want to memorize, create a JSON file in the root of your project folder. Mine is called most-common-german-words.json. Feel free to download it if you want to use it to follow this tutorial.
Creating the service
We’re going to create a simple script that picks a random word from the JSON file and then sends it to a phone number via SMS. That’s it. With this script, you can use the scheduler you like to keep it running (e.g. Crontab, Heroku Scheduler).
Below you can find our service code. Copy it into a file called learning_german.py
:
By the beginning of this file, we’re creating a Twilio REST Client. We’re going to use our Twilio’s Account SID and Auth Token to set it up (from the Twilio Console). A Client constructor without parameters will look for the TWILIO_ACCOUNT_SID
and TWILIO_AUTH_TOKEN
variables inside the current environment. In the comments you can see an alternative way of adding these variables to your code - which is not recommendable, in order to avoid security issues, like pushing it to a public repository by mistake.
Go to the terminal, and create an environment variable called TWILIO_ACCOUNT_SID
and another called TWILIO_AUTH_TOKEN
. On Mac OS/Linux this can be done by:
For those of you following the tutorial on Windows, enter the following command in a command prompt window:
Also, we’re hard coding two phone numbers in this line:
The first one is the number on which you will receive the messages, and the second one is the number that is sending it (your Twilio phone number). Don’t forget to replace these phone numbers with yours, using the E.164 format.
Let’s check if our script works:
Nothing happened in the console? Have you checked your phone? :) If everything went fine, you must have received an SMS that looks like this:
Testing your service
Our code is working just fine but, as every other software, most likely it will grow and change - and we have to make sure that the main functionality continues to work. That’s why we write code to test our code.
There are tons of testing strategies and each of them will cover a different need. Read more about it in the classic Martin Fowler’s blog post on the Test Pyramid. Here we’re going to see how to unit test your Twilio API calls.
In this blog post we’re using pytest
to write tests in a clean and simple way. If you use Python’s built in test library (unittest
) don’t worry; pytest
got you covered, since its runner is compatible with tests written for unittest
as well.
pytest 101
Writing a test with pytest only requires you to create a test file and a test method, both starting with the test_
prefix (this can be changed by you if you want to). Let’s create a file called test_learning_german.py
.
It’s a good practice to name the method after the module name. Here, for instance, the module name is learning_german.py
, so our test module is test_learning_german.py
.
Our service has two methods: send_message
and send_a_german_word
. We’ll focus on testing the one that makes calls to Twilio: send_message
. Let’s recap how our method looks like:
The method receives a phone number to send the message, a phone number which is sending the message and the message itself. If the message is sent, it returns the message attribute sid
; if not, it will log an error and return None
.
Our first test will check if a message was sent. Copy the code below to your test_learning_german.py
file:
Run your test using:
That’s right, you don’t have to point to the test file or folder, since pytest will discover it on its own. Also, make sure to have valid phone numbers in your test for now - we’ll change this later.
If everything went fine, you will see something like this:
Yay! Our first test just ran. But it has a few problems:
- Every time you run it, it will send real SMSs - we don’t want this
- Every time it sends a real SMS, Twilio charges you money
- You need real phone numbers to test your code
- It is slow
We can solve these problems using two different unit test strategies: Mocks and Stubs.
Mocking parts of your code
Mocking is replacing some part of your code with a fake object that may or may not simulate how it should work. In our case, we want to mock the part of our code that needs an API connection. Python has a built in module for mocking and asserting its calls. Read more about Python’s mock object library.
We have run our code and we know that it works. We can now mock the part of it that does real calls to the Twilio API: client.messages.create()
. Instead of having it connecting to Twilio’s servers, we’re going to replace our previous test with the next one, that uses mocks.
Let’s see how Python’s mock module looks like:
After importing the mock
module, we add a decorator to our test. This decorator has a path to the method we want to fake: @mock.patch('learning_german.client.messages.create')
. The mock object, created by this decorator, will be injected as an argument to our test method: def test_send_a_common_word(create_message_mock):
.
As part of mocking the method, we also need to return something once it is called, after all, the method send_message
returns a sid
that comes from client.messages.create()
. We use return_value
to indicate that every time this mock is called, it should return something. In our case, we only need it to return an attribute called sid
: create_message_mock.return_value.sid = expected_sid
. Since there isn’t going to be an actual call to the Twilio API, we use a made up sid
.
After configuring a mock object, we’re ready to call the method, as we did before, but the mock object will remember how it is used, so after having it called, we can check it. Python’s mock library offers us a few options to check the status of a mock. Here we’re using called
but you can use others such as:
- assert_called
- assert_called_once_with
- assert_has_calls
- assert_not_called
- call_count
Time to run our tests again:
You can see now that our tests are faster than before and no actual calls to the Twilio API were made. You can monitor the calls from your Twilio’s token in the Twilio Console.
Mocking exceptions
Apart from the “happy path”, our SMS application has a block to catch exceptions. It’s good if we make sure that this part of the application works too. We’re going to create another test; this time to check if our program is logging an error whenever an exception from Twilio’s library is thrown. Copy the test below after our previous test in your test_learning_german.py
:
Here we have two different arguments in our test: the first, you already know, is our mock object; the second one is the caplog
Pytest fixture, useful for capturing the writes from the standard output. Read more about Pytest fixtures here.
This test is a bit different from the previous one; we want it to simulate an exception being thrown. We simulate an unexpected behavior by using the attribute side_effect
. By doing this we make sure that the exception assigned to side_effect
will be thrown when the mock is called, and in this way the code will simulate an error and lead to the except block of our function.
Now we have to make sure that the expected message is logged and the method returns None
:
Stubs
What if you want to run the real code or the fake code depending on the environment?
Another strategy to fake real calls in unit testing is using stubs. A stub is a piece of code that will be injected as a replacement for the real thing and will behave like it. Read more about the difference between Mocks and Stubs here.
Let’s see how a stub looks like. Add the following code to learning_german.py
, before the method send_message
(please mind the new imports):
We created a FakeClient
to simulate the behavior of client.messages.create()
. No matter how the input is, it will always return a made up sid
value that looks the same as the real ones. If you want to use a real connection, you just have to change the ENVIRONMENT
variable in the environment to prod
(any value different than dev
will also work).
Below you can see how our test would look like if we use this approach instead of mocks. You can add it at the bottom of test_learning_german.py
:
Same logic but no mocks. Cleaner and can be used to run the test against the real API (don’t forget to add valid numbers).
Everything together
Here is how the final versions of our service and tests look like:
learning_german.py
test_learning_german.py
With this test module you can see both strategies working together. You may also play with the environment variable ENVIRONMENT
set as dev
or prod
to see the difference between the execution times.
Conclusion
Testing our code is the best way to make sure that we’re delivering the best experience we can to our customers. Tested code is code that will alert you of bugs before it goes to production, making developers feel confident about their changes, and also document what is expected from it.
Mocks are useful when you need to come up with different scenarios for different tests. If your code is full of nested conditions, different behaviour for different exceptions, and so on, mocks are your go to solution.
But if APIs calls are made from different parts of your code and you don’t see value in mocking them every single time, then stubs are made for you. Also, remember that you don’t have to choose one in favor of the other. You can always combine both strategies.
I hope you liked this tutorial and that it has given you some new insights. Can’t wait to hear what tests you build!
Ana Paula Gomes is a software engineer, a runner wannabe and crazy open source lady.
- Github: https://github.com/anapaulagomes
Related Posts
Related Resources
Twilio Docs
From APIs to SDKs to sample apps
API reference documentation, SDKs, helper libraries, quickstarts, and tutorials for your language and platform.
Resource Center
The latest ebooks, industry reports, and webinars
Learn from customer engagement experts to improve your own communication.
Ahoy
Twilio's developer community hub
Best practices, code samples, and inspiration to build communications and digital engagement experiences.