Testing
Masonite testing is very simple. You can test very complex parts of your code with ease by just extending your class with a Masonite unit test class.
Although Masonite uses
pytest
to run tests, Masonite's test suite is based on unittest
. So you will use unittest
syntax but run the tests with Pytest. Because of this, all syntax will be in camelCase
instead of PEP 8 under_score
. Just know that all TestCase
method calls used during testing is in camelCase
form to maintain unittest standards.Normal tests should still be underscore and start with
test_
though like this:def test_user_can_login(self):
pass
First, create a new test class in a testing directory. There is a craft command you can run to create tests for you so just run:
$ craft test User
This will create a user test for us which we can work on. You can drag this test in any subdirectory you like.
This command will create a basic test like the one below:
tests/test\user.py
"""Example Testcase."""
from masonite.testing import TestCase
class TestUser(TestCase):
transactions = True
def setUp(self):
super().setUp()
def setUpFactories(self):
pass
That's it! You're ready to start testing. Read on to learn how to start building your test cases.
Most times you want to develop and test on different databases. Maybe you develop on a local MySQL database but your tests should run in a SQLlite database.
You can create a
.env.testing
file and put all database configs in that. When Pytest runs it will additionally load and override any additional environment variables.Your
.env.testing
file may look like this:DB_CONNECTION=sqlite
DB_HOST=127.0.0.1
DB_DATABASE=masonite.db
DB_LOG=False
STRIPE_CLIENT=test_sk-9uxaxixjsxjsin
STRIPE_SECRET=test_sk-suhxs87cen88h7
Feel free to load any testing environment variables in here. By default they will not be commited.
Here is a list of methods that can be used for assetions.
All methods that begin with
assert
can be chained together to run through many assertions. All other method will return some kind of boolean or value which you can use to do your own assertions.assertContains(value) | assertHasAmount(amount) | assertHeaderIs(key, value) |
assertNotFound() | assertNotHasAmount(amount) | assertPathIs(value) |
assertHasJson(key, value) | assertParameterIs(parameter, value) | isNamed(name) |
assertJsonContains(key, value) | assertIsStatus(code) | hasMiddleware(*middleware) |
assertCount(number) | assertHasHeader(name) | hasController(name) |
contains(value) | ok() | canView() |
hasJson(key, value) | count(number) | amount(number) |
isGet() | isPost() | isPut() |
isPatch() | isDelete() | hasSession(key, value) |
parameterIs() | headerIs() | |
We have a few options for testing our routes.
To check if a route exists, we can simple use either get or post:
tests/test_unit.py
def test_route_exists(self):
self.assertTrue(self.get('/testing'))
self.assertTrue(self.post('/testing'))
The request and responses of a test are gotten by accessing the
request
and response
attributes. The response
attribute will be a string representation of your route:def test_request_and_response(self):
request = self.get('/testing').request # <masonite.request.Request>
response = self.get('/testing').response # HTML code
You can choose anyone of the normal request methods:
def test_route_exists(self):
self.get('/testing')
self.post('/testing')
self.put('/testing')
self.patch('/testing')
self.delete('/testing')
You can use a standard JSON request and specify whichever option you need using the
json()
method:def test_route_exists(self):
self.json('POST', '/testing', {'user': 'Joe'})
tests/test_unit.py
def test_route_has_the_correct_name(self):
self.assertTrue(self.get('/testing').isNamed('testing.route'))
tests/test_unit.py
def test_route_has_route_middleware(self):
assert self.get('/testing').hasMiddleware('auth', 'owner')
This can be used to see if the template returned a specific value
tests/test_unit.py
def test_view_contains(self):
assert self.get('/login').contains('Login Here')
You can also use:
self.get('/login').assertContains('Login Here')
You can easily check if the response is ok by using the
ok
method:tests/test_unit.py
def test_view_is_ok(self):
assert self.get('/testing').ok()
By default, all calls to your routes with the above methods will be without CSRF protection. The testing code will allow you to bypass that protection.
This is very useful since you don't need to worry about setting CSRF tokens on every request but you may want to enable this protection. You can do so by calling the
withCsrf()
method on your test.def test_csrf(self):
self.withCsrf()
self.post('/unit/test/json', {'test': 'testing'})
This will enable it on a specific test but you may want to enable it on all your tests. You can do this by adding the method to your
setUp()
method:def setUp(self):
super().setUp()
self.withCsrf()
def test_csrf(self):
self.post('/unit/test/json', {'test': 'testing'})
As you have noticed, Masonite has exception handling which it uses to display useful information during development.
This is an issue during testing because we wan't to be able to see more useful testing related issues. Because of this, testing will disable Masonite's default exception handling and you will see more useful exceptions during testing. If you want to use Masonite's built in exception handling then you can enable it by running:
def setUp(self):
super().setUp()
self.withExceptionHandling()
def test_csrf(self):
self.post('/unit/test/json', {'test': 'testing'})
You can get the output by using the capture output easily by calling the
captureOutput
method on your unit test:def test_get_output(self):
with self.captureOutput() as o:
print('hello world!')
self.assertEqual(o, 'hello world!')
A lot of times you will want to build tests around your API's. There are quite a few methods for testing your endpoints
You can test to make sure your endpoint returns a specific amount of something. Like returning 5 articles:
def test_has_articles(self):
self.assertTrue(
self.json('GET', '/api/articles').count(5)
)
You can also use
assertCount(5)
:def test_has_articles(self):
self.json('GET', '/api/articles').assertCount(5)
You can also use
amount
which is just an alias for count
:def test_has_articles(self):
self.assertTrue(
self.json('GET', '/api/articles').amount(5)
)
self.json('GET', '/api/articles').assertHasAmount(5)
self.json('GET', '/api/articles').assertNotHasAmount(10)
You can also check if a specific JSON key has a specific amount. For example:
"""
{
"name": "Joe",
"tags": ['python', 'framework'],
"age": 25
}
"""
def test_has_several_tags(self):
self.assertTrue(
self.json('GET', '/api/user').hasAmount('tags', 2)
)
Sometimes you will want to check if your endpoint returns some specific JSON values:
"""
{
"name": "Joe",
"title": "creator",
"age": 25
}
"""
def test_has_age(self):
self.assertTrue(
self.json('GET', '/api/user').hasJson('age', 25)
)
You can also specify a dictionary of values as well and will check if each value inside the response:
"""
{
"name": "Joe",
"title": "creator",
"age": 25
}
"""
def test_has_age(self):
self.assertTrue(
self.json('GET', '/api/user').hasJson({
"name": "Joe",
"age": 25
})
)
You do not have to specify all of the elements. Just the ones you want to check for.
You can alo use:
self.assertJsonHas('key', 'value')
You can also assert values inside a list of responses:
"""
[
{
"name": "Joe",
"title": "creator",
"age": 25
},
{
"name": "Bob",
"title": "Co-Founder",
"age": 26
}
]
"""
def test_bob_in_result(self):
self.json('GET', '/api/user').assertJsonContains('name', 'Bob')
You can also use dot notation for multi dimensional endpoints:
"""
{
"profile": {
"name": "Joe",
"title": "creator",
"age": 25
}
}
"""
def test_has_name(self):
self.assertTrue(
self.json('GET', '/api/user').hasJson('profile.name', 'Joe')
)
Sometimes you don't want to use dot notation and may choose to convert directly to a dictionary and assert values on that. This is also good for debugging so you can print the dictionary to these terminal. You can do this easily:
"""
{
"profile": {
"name": "Joe",
"title": "creator",
"age": 25
}
}
"""
def test_has_name(self):
dictionary = self.json('GET', '/api/user').asDictonary()
self.assertEqual(dictionary['profile']['name'], 'Joe')
You can test if a specific parameter contains a specific value. For example if you want to see if the parameter
id
is equal to 5
:def test_has_name(self):
# Route is: /dashboard/user/@id
self.assertTrue(
self.get('GET', '/dashboard/user/5').parameterIs('id', '5')
)
self.get('GET', '/dashboard/user/5').assertParameterIs('id', '5')
You can test if a specific header contains a specific value. For example if you want to see if the header
Content-Type
is equal to text/html
:def test_has_name(self):
# Route is: /dashboard/user/@id
self.assertTrue(
self.get('GET', '/dashboard/user/5').headerIs('Content-Type', 'text/html')
)
self.get('GET', '/dashboard/user/5').assertHeaderIs('Content-Type', 'text/html')
You can use the
isStatus
and assertIsStatus
methods to assert status checks:self.assertTrue(
self.get('GET', '/dashboard/user/5').isStatus(200)
)
self.get('GET', '/dashboard/user/5').assertIsStatus(200)
You can also easily assert
404
methods:self.get('GET', '/dashboard/not/exists').assertNotFound()
This is the same as asserting a
404
status code.By default, Masonite turns off subdomains since this can cause issues when deploying to a PaaS that deploys to a subdomain like
sunny-land-176892.herokuapp.com
for example.To activate subdomains in your tests you will have to use the
withSubdomains()
method. You can then set the host in the wsgi
attribute.def test_subdomains(self):
self.withSubdomains().get('/view', wsgi={
'HTTP_HOST': 'subb.domain.com'
}).assertIsStatus(404)
By default, to prevent messing with running databases, database test cases are set to only run on the
sqlite
database. You can disable this by setting the sqlite
attribute to False
.from masonite.testing import TestCase
class TestUser(TestCase):
"""Start and rollback transactions for this test
"""
transactions = True
sqlite = False
def setUp(self):
super().setUp()
This will allow you to use whatever database driver you need.
By default, all your tests will run inside a transaction so any data you create will only exist within the lifecycle of the test. Once the test completes, your database is rolled back to its previous state. This is a perfect way to prevent test data from clogging up your database.
Although this is good for most use cases, you may want to actually migrate and refresh the entire migration stack. In this case you can set the
refreshes_database
attribute to True
.from masonite.testing import TestCase
class TestUser(TestCase):
"""Start and rollback transactions for this test
"""
transactions = False
refreshes_database = True
def setUp(self):
super().setUp()
Now this will migrate and refresh the database.
Beware that this will destroy any database information you have.
Factories are simply ways to seed some dummy data into your database. You can create a factory by making a method that accepts a faker argument and using that to seed data.
Masonite has a convenient method you can use that will run once the test first boots up called
setUpFactories
. This will run once and only once and not between every test.Let's create the factory as well as use the setUpFactories method to run them now.
Below is how to create 100 users:
from masonite.testing import TestCase
from app.User import User
class TestUser(TestCase):
"""Start and rollback transactions for this test
"""
transactions = True
def setUp(self):
super().setUp()
def setUpFactories(self):
self.make(User, self.user_factory, 100)
def user_factory(self, faker):
return {
'name': faker.name(),
'email': faker.email(),
'password': '$2b$12$WMgb5Re1NqUr.uSRfQmPQeeGWudk/8/aNbVMpD1dR.Et83vfL8WAu',
# == 'secret'
}
def test_creates_users(self):
pass
You don't need to build factories though. This can be used to simply create new records:
from masonite.testing import TestCase
from app.User import User
class TestUser(TestCase):
"""Start and rollback transactions for this test
"""
transactions = True
def setUp(self):
super().setUp()
def setUpFactories(self):
User.create({
'name': 'Joe',
'email': '[email protected]',
'password': '$2b$12$WMgb5Re1NqUr.uSRfQmPQeeGWudk/8/aNbVMpD1dR.Et83vfL8WAu', # == 'secret'
})
def test_creates_users(self):
pass
We can load users into the route and check if they can view the route. This is good to see if your middleware is acting good against various users. This can be done with the
acting_as()
method.tests/test_unit.py
from app.User import User
...
def setUpFactories(self):
User.create({
'name': 'Joe',
'email': '[email protected]',
'password': '$2b$12$WMgb5Re1NqUr.uSRfQmPQeeGWudk/8/aNbVMpD1dR.Et83vfL8WAu', # == 'secret'
})
def test_user_can_see_dashboard(self):
self.assertTrue(
self.actingAs(User.find(1)).get('/dashboard').ok()
)
Maybe you need to check a post request and pass in some input data like submitting a form. You can do this by passing in a dictionary as the second value to either the
get
or post
method:def test_user_can_see_dashboard(self):
self.assertTrue(
self.actingAs(User.find(1)).post('/dashboard', {
'name': 'Joe',
'active': 1
})
)
The same can be applied to the get method except it will be in the form of query parameters.
Instead of doing model calls to check if values exist in the database, you can use a simple
assertDatabaseHas
assertion:def test_create_article(self):
self.assertTrue(
self.post('/articles', {
'name': 'Masonite is great',
'author_id': 1
}).assertDatabaseHas('articles.name', 'Masonite is great')
)
You can also do the same thing with the opposite
assertDatabaseNotHas
assertion:def test_create_article(self):
self.assertTrue(
self.post('/articles', {
'name': 'Masonite is great',
'author_id': 1
}).assertDatabaseNotHas('articles.name', 'Masonite is bad')
)
To complete our test, let's check if the user is actually created:
from masonite.testing import TestCase
from app.User import User
class TestUser(TestCase):
"""Start and rollback transactions for this test
"""
transactions = True
def setUp(self):
super().setUp()
def setUpFactories(self):
User.create({
'name': 'Joe',
'email': '[email protected]',
# == 'secret'
'password': '$2b$12$WMgb5Re1NqUr.uSRfQmPQeeGWudk/8/aNbVMpD1dR.Et83vfL8WAu',
})
def test_creates_users(self):
self.assertTrue(User.find(1))
Thats it! This test will now check that the user is created properly
You can run tests by running:
$ python -m pytest
Last modified 3yr ago