Ensuring Clean Django Views: A Test-Driven Approach

Published: 9 months ago - Updated: 9 months ago

5 minutes - 810 Words

article 'Ensuring Clean Django Views: A Test-Driven Approach' banner

Summary

Ensuring Clean Django Views: A Test-Driven Approach

Introduction:

A few days ago, I was watching a talk about Django best practices for scalable project structures, I came across a useful suggestion that emphasizes keeping the views.py file clear.

“Your views.py should only have views.”

The advice is obvious. Move everything that is not a view out of views.py and establish a clear difference between view-related and utility functions.

This advice stuck with me because I have faced this issue before, where I was going through views.py file and didn’t know whether a function in views.py was intended for a view or was it just a utility function? This ambiguity might cause codebase confusion, making it difficult to determine the function’s purpose.

Enforcing the Rule:

Two conventional ways to enforce this rule within a development team are:

Way 1: Communicate and Inform

The first way is by communicating the rule clearly to everyone on the team and enforcing it through pull request reviews. However effective, this method requires continuous effort and monitoring and is prune to human made errors. That is why I went with the second option.

Way 2: Test-Driven Enforcement

I investigated putting the rule into our automated tests to speed up the enforcement process and remove active intervention and monitoring from the process.

The Test:

The test involves three key steps:

  1. Get all functions from the view:

    In the test file, we create a helper method to obtain all of the view functions. The method excludes utility functions from consideration by filtering out functions that begin with an underscore, those that are not callable and those that are imported.

    # Define a helper method to get all the view functions in views.py
    def get_view_functions(self):
        # Get all the attributes of the views module
        attrs = dir(views)
        # Filter out the ones that start with underscore or are not callable
        funcs = [getattr(views, attr) for attr in attrs if not attr.startswith("_") and callable(getattr(views, attr))]
        # Filter out the ones that are imported from other modules
        funcs = [
            func for func in funcs if inspect.getmodule(func) == views
        ] 
        # Return the list of functions
        return funcs
    
  2. Get all URL patterns for that view:

    Get the urls from url.py file from the app that we are testing. In this case it is the base app.

    from base.urls import urlpatterns
    
  3. Compare the functions and URLs:

    We create a test method that checks if all view functions are used in the imported URL patterns:

    def test_view_functions_are_used_in_urls(self):
        # Get all the view functions and urlpatterns
        view_funcs = self.get_view_functions()
        app_urls = urlpatterns
    
        # Loop through each function
        for func in view_funcs:
            found = False
            for url in app_urls:
                if func is url.callback:
                    found = True
                    break
    
            if not found:
                self.fail(f"{func.__name__} is not used in urls")
    

    The test loops through each function, testing to see if it matches a URL pattern callback. The test fails if a function is not found in any URL patterns, suggesting that the function is not being used as a view.

Complete Test Suite:

The full test suite ensures that all functions in views.py are utilized in the URL patterns, providing a safeguard against inadvertently unused functions:

class ViewTestCase(TestCase):
    # Define a helper method to get all the view functions in views.py
    def get_view_functions(self):
        # Get all the attributes of the views module
        attrs = dir(views)
        # Filter out the ones that start with underscore or are not callable
        funcs = [getattr(views, attr) for attr in attrs if not attr.startswith("_") and callable(getattr(views, attr))]
        # Filter out the ones that are imported from other modules
        funcs = [
            func for func in funcs if inspect.getmodule(func) == views
        ]  # Check if the function's module is the same as the views module
        # Return the list of functions
        return funcs

    # Define a test method to check if all the view functions are used in urls
    def test_view_functions_are_used_in_urls(self):
        # Get all the view functions
        view_funcs = self.get_view_functions()
        view_urls = urlpatterns

        # Loop through each function
        for func in view_funcs:
            found = False
            for url in view_urls:
                if func is url.callback:
                    found = True
                    break

            if not found:
                self.fail(f"{func.__name__} is not used in urls")

By running this test, every function declared in views.py that does not have a corresponding usage in the URL patterns will result in a test failure, alerting the development team to the problem.

desc

Conclusion

Maintaining a scalable and understandable codebase requires enforcing a clean, consistent and transparent structure.

The test-driven methodology given here provides an automated and dependable method for ensuring that functions written in views.py file are actively used in the URLs, which make our code predicable and consistent.

Add Comment

Conversations (0)

Sign up for our newsletter

Stay up to date with the latest news and articles.