This post shows you a short example of how to use the Python module moto to mock a SQS queue. This technique is useful when writing code level tests for applications hosted on an AWS stack. We will work with a method that takes a string as an input, processes the string and then writes it to an SQS queue. We will use moto to mock the SQS queue and verify that the right message gets sent to the queue.
Background
You can safely skip this section if you do not care for where this work came from. Qxf2 failed on a client project recently. We had test an application that was hosted entirely on the AWS stack. The application was made of several pipelines that each consisted of loosely coupled microservices (lambdas), used s3 buckets as a datastore and were connected by messaging queues (SQS) and message broadcasters (SNS). While this sort of arrangement sounds straightforward to test, we struggled massively. We quickly realized that we lacked the right tooling to aid our testing. So, for the last few months, we have been working on fixing that hole. This post documents one of our learnings along the way. We hope that our learning helps other professional testers who are also struggling to test similar applications.
The method under test
Our method under test is a simplified version of what writing to a SQS queue using Python looks like. We take a couple of inputs, create a message out of it and then write it to a queue. While this example looks contrived, it covers the most critical case I want to show as part of this blog post.
QUEUE_URL = 'blah blah blah' def write_message(daily_message, channel): "Send a message to Skype Sender" sqs = boto3.client('sqs') message = str({'msg':f'{daily_message}', 'channel':channel}) sqs.send_message(QueueUrl=QUEUE_URL, MessageBody=(message)) |
Let us assume this code exists in a file called daily_messages.py
.
Using moto’s @mock_sqs
The Python module moto makes it really easy for us to mock this operation. moto provides the @mock_sqs decorator that mocks out an SQS queue. Our way of writing this one test will involve the following steps:
1. Decorate the test method with @mock_sqs
2. Create an SQS resource
3. Create a queue
4. Force daily_messages.write_message() to use the newly created queue
5. Create inputs for daily_messages.write_message()
6. Arrive at an expected result
7. Call daily_messages.write_message()
8. Read the queue for messages
9. Verify that the message body matches the expected result
1. Decorate the test method with @mock_sqs
Create a new file called test_daily_messages.py
and add the following lines:
import boto3 from moto import mock_sqs import daily_messages @mock_sqs def test_write_message_valid(): "Test the write_message method with a valid message" |
At this stage, we have imported the necessary modules and decorated our test method with @mock_sqs.
2. Create an SQS resource
Now, within the test_write_message_valid()
method, create an SQS resource like this
sqs = boto3.resource('sqs') |
3. Create a queue
You can create a queue using the SQS resource like this
queue = sqs.create_queue(QueueName='test-skype-sender') |
4. Force daily_messages.write_message() to use the newly created queue
If you notice daily_messages.py
uses a global variable called QUEUE_URL to select the queue being used. So let us obtain the queue URL of our newly created queue and set that as daily_messages.QUEUE_URL
.
daily_messages.QUEUE_URL = queue.url |
Tip: To find out more about what the queue
object contains, you can print out queue.__dict__
and queue.attributes
.
5. Create inputs for daily_messages.write_message()
This is the part where testers shine. For this example, I will just show one tame combination of input to daily_messages.write_message()
but when testing a real app, go crazy with your inputs!
skype_message = 'Testing with a valid message' channel = 'test' |
6. Arrive at an expected result
Understand the transformation that happens in daily_messages.write_message()
to arrive at an expected result. We notice daily_messages.write_message()
takes the two input arguments, creates a simple dictionary with keys msg
and channel
and finally converts the dictionary into a string. To arrive at our expected result, we will simply do the same.
expected_message = str({'msg':f'{skype_message}', 'channel':channel}) |
For folks used to the Arrange, Act, Assert style, we have just completed the Arrange portion.
7. Call daily_messages.write_message()
daily_messages.write_message(skype_message, channel) |
And at this point, when daily_messages.write_message() is executed, it should end up populating the SQS queue we created in step 3. We are done with the Act portion of the Arrange, Act, Assert pattern.
8. Read the queue for messages
moto follows the same patterns as boto3. So if you are familiar with boto3, you could have simply guessed that queue.receive_messages()
was possible on the queue
object.
sqs_messages = queue.receive_messages() |
9. Verify that the message body matches the expected result
Let’s assert that the body of the message looks right
assert sqs_messages[0].body == expected_message, 'Message in skype-sender does not match expected' |
Tip: You can also assert other things. For example, you can verify that exactly one message was sent using assert len(sqs_messages) == 1, 'Expected exactly one message in SQS'
.
Putting it all together
Here is how our test looks:
""" Example of using moto to mock out an SQS queue """ import boto3 from moto import mock_sqs import daily_messages @mock_sqs def test_write_message_valid(): "Test the write_message method with a valid message" sqs = boto3.resource('sqs') queue = sqs.create_queue(QueueName='test-skype-sender') daily_messages.QUEUE_URL = queue.url skype_message = 'Testing with a valid message' channel = 'test' expected_message = str({'msg':f'{skype_message}', 'channel':channel}) daily_messages.write_message(skype_message, channel) sqs_messages = queue.receive_messages() assert sqs_messages[0].body == expected_message, 'Message in skype-sender does not match expected' print(f'The message in skype-sender SQS matches what we sent') assert len(sqs_messages) == 1, 'Expected exactly one message in SQS' print(f'\nExactly one message in skype-sender SQS') |
If you are a tester working on testing different aspects of an AWS pipeline, consider using mocking as part of your tests. I hope this article simplifies the thought process behind using moto for mocking an AWS SQS queue.
I want to find out what conditions produce remarkable software. A few years ago, I chose to work as the first professional tester at a startup. I successfully won credibility for testers and established a world-class team. I have lead the testing for early versions of multiple products. Today, I run Qxf2 Services. Qxf2 provides software testing services for startups. If you are interested in what Qxf2 offers or simply want to talk about testing, you can contact me at: [email protected]. I like testing, math, chess and dogs.
hi thanks for writing this post up. there are several issues with the code but i have posted some code below to help future readers. note: you should ensure you stub the AWS_CREDENTIALS for your mock_*s to make sure you never accidentally write to a real aws resources. Below i use conftest to setup a mock endpoint:
# conftest.py
@pytest.fixture(scope=’function’)
def aws_credentials():
“””Mocked AWS Credentials for moto.”””
os.environ[‘AWS_ACCESS_KEY_ID’] = ‘testing’
os.environ[‘AWS_SECRET_ACCESS_KEY’] = ‘testing’
os.environ[‘AWS_SECURITY_TOKEN’] = ‘testing’
os.environ[‘AWS_SESSION_TOKEN’] = ‘testing’
REGION = ‘us-east-1′
@pytest.fixture(scope=’function’)
def sqs_client(aws_credentials):
# setup
with mock_sqs():
yield boto3.client(‘sqs’, region_name=REGION)
# teardown
# test_sqs.py
def test_write_message(sqs_client):
queue = sqs_client.create_queue(QueueName=’test-msg-sender’)
queue_url = queue[‘QueueUrl’]
# override function global URL variable
chasm.CLAIMANT_QUEUE_URL = queue_url
expected_msg = str({‘msg’:f’this is a test’})
chasm.write_message(expected_msg)
sqs_messages = sqs_client.receive_message(QueueUrl=queue_url)
assert json.loads(sqs_messages[‘Messages’][0][‘Body’]) == expected_msg
# app.py
def write_message(data):
sqs = boto3.client(‘sqs’, region_name = ‘us-east-1’)
r = sqs.send_message(
MessageBody = json.dumps(data),
QueueUrl = CLAIMANT_QUEUE_URL
)
The code you have written seems to implement `moto.mock_sqs`. That is why you do not need to import `moto`.
The moto module already does what you are showing in your example code. When working with a mock provided by moto, there is no need for credentials and access setup in the first place. So there is very little chance (you literally have to do extra setup) of writing to a real queue. The test I show will work on its own and does not rely on any access/credentials in the first place.
thanks for writing this up! there were a few issues with the code so i published a fully working example here: https://dev.to/drewmullen/mock-a-sqs-queue-with-moto-4ppd
Cool! I think what you have done here is show how to do what `moto` does in pure Python. So you might want to change your title to reflect that. Notice you have not imported `moto` anywhere and yet you have `sqs_client` as a fixture to your test.
Hi,
Do you have any idea how do the same test in async?
Hi milad,
You an look up aioboto3 which allows you to use the boto3 client commands in an async manner. You could also make use of python’s built-in
concurrent.futures
library