DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Joseph Mancuso for Masonite

Posted on • Updated on

Testing Controller Logic With Masonite

Introduction

One of the reasons controllers are structured like they are is because of how testable they become. In fact, Masonite itself is extremely testable. The main package has a plethora of features, since it houses the entire framework, and is still ~91% code coverage.

Let’s give an example of a typical controller and show how we can modify it to be more testable. I feel like the best way to go through this article is to give a real world example and then how we can modify it to pass tests.

Masonite is very testable but it does allow some ways to do things that are not testable. For example, Masonite comes with builtin helpers which makes various parts of your code not easily tested without mocking a builtin (which is sort of weird).

I’ll be creating some of these articles on how to refactor various controller methods so be sure to give a follow.

Our Example

We’ll keep it very simple here so we can go through all the basics without losing anyone. Here is our example Masonite controller:


class PlanController:
    """ Manage User Subscriptions """

    def subscribe(self):
        plan = request().input('plan_id')
        request().user().subscribe(plan, request().input('stripeToken'))

        return request().redirect('/plans')
Enter fullscreen mode Exit fullscreen mode

Problem 1

If we go to test this, it might look something like this:

from app.http.controllers.PlanController import PlanController

class TestPlanController:

    def setup_method(self):
        self.controller = PlanController()

    def test_controller_subscribes_user(self):
        assert self.controller.show()
Enter fullscreen mode Exit fullscreen mode

This will start throwing a whole bunch of errors because:

  1. Python testing doesn’t know what the hell a request() is because it’s a builtin function that is activated by a Service Provider when the application first boots up.
  2. It’s not correctly able to get the input because we never specified an input.

Refactoring

Let’s refactor the controller so we can at least pass in a request:

from masonite.request import Request

class PlanController:
    """ Manage User Subscriptions """

    def subscribe(self, request: Request):
        plan = request.input('plan_id')
        request.user().subscribe(plan, request.input('stripeToken'))

        return request.redirect('/plans')
Enter fullscreen mode Exit fullscreen mode

Ok awesome. Now we can at least pass in the request class so let’s go back to our controller test:

from app.http.controllers.PlanController import PlanController
from masonite.request import Request
from masonite.testsuite.TestSuite import generate_wsgi

class TestPlanController:

    def setup_method(self):
        self.request = Request(generate_wsgi())
        self.controller = PlanController()

    def test_controller_subscribes_user(self):
        assert self.controller.show(self.request)
Enter fullscreen mode Exit fullscreen mode

Perfect! Now we can at least mock the request class. We didn’t really need to mock the request class because we can just use the real class and mock the WSGI request.

Notice though that we imported a generate_wsgi function into the code from the Masonite test suite. Masonite has a few helper classes and functions used for testing which we can go through in another article.

Problem 2

Ok so this should STILL not work because we didn’t actually load in a user into the request.user() method. It should always return None.

Let’s have a request class where we have a user already loaded in. This user will require a subscribe method and we will also check if the user is subscribed so let’s just mock that up.

We will also mock up a new request class so we can add any methods we need to it in the future:

from app.http.controllers.PlanController import PlanController
from masonite.request import Request
from masonite.testsuite.TestSuite import generate_wsgi

class MockUser:
    def subscribe(self, plan, token):
        self.plan = plan

    def is_subscribed(self, plan):
        if self.plan == plan:
            return True

        return False

class MockRequest(Request):

    def user(self):
        return MockUser

class TestPlanController:

    def setup_method(self):
        self.request = MockRequest(generate_wsgi())
        self.controller = PlanController()

    def test_controller_subscribes_user(self):
        assert self.controller.show(self.request)
Enter fullscreen mode Exit fullscreen mode

Awesome. Now we can run the request.user() method and it will return this MockUser class.

Problem 3

We do not have the correct inputs now. We can also easily mock those up:

from app.http.controllers.PlanController import PlanController
from masonite.request import Request
from masonite.testsuite.TestSuite import generate_wsgi

...

class TestPlanController:

    def setup_method(self):
        self.request = MockRequest(generate_wsgi())
        self.controller = PlanController()

    def test_controller_subscribes_user(self):
        # Sets input data
        self.request.request_variables = {'plan_id': 'premium', 'stripeToken': 'tok_amex'}

        assert self.controller.show(self.request)
Enter fullscreen mode Exit fullscreen mode

GREAT! Now we have everything we need for the test to pass correctly. Now let’s just do 1 more assertion:

    ...
    ...
    def test_controller_subscribes_user(self):
        # Sets input data
        self.request.request_variables = {'plan_id': 'premium', 'stripeToken': 'tok_amex'}

        assert self.controller.show(self.request)
        assert self.request.user().is_subscribed()
Enter fullscreen mode Exit fullscreen mode

We have successfully verified that this controller method successfully subscribes users!

Top comments (0)

Meeting a new developer

Stop by this week's meme thread!