Research on Unit Testing Tools - SubarnaSaha08/JUMCMS-Jahangirnagar-University-Medical-Center-Management-System GitHub Wiki

What does Unit Testing mean?

Unit Testing is a fundamental aspect of software testing where individual components or functions of a software application are tested in isolation. This method ensures that each unit of the software performs as expected. By focusing on small, manageable parts of the application, unit testing helps identify and fix bugs early in the development process, significantly improving code quality and reliability.

Unit tests are typically automated and written by developers using various frameworks such as JUnit, NUnit, or pytest. These tests validate the correctness of code by checking that each function or method returns the expected.

Django’s Built-in Test Framework

Author

Subarna Saha

Introduction

Django’s built-in test framework refers to the set of tools and features provided by Django to facilitate testing in web applications. It is built on top of Python's standard unittest module and allows developers to create and run tests to ensure their Django applications work as expected. This framework helps automate the testing process, ensuring that code behaves as intended, and helps catch issues early.

Features

Integration with unittest

Django’s test framework is based on Python’s unittest module, which provides tools for creating unit tests. It extends unittest to make it easier to test Django-specific features like views, models, forms, and templates.

Test Database

Django automatically creates a test database when tests are run. This test database is a temporary copy of your development database, and it’s used to isolate tests from real data. Once the tests are completed, Django automatically destroys the test database.

Client for Simulating Requests

The TestCase class in Django provides a client object that can be used to simulate GET and POST requests. This allows you to test how your views respond to different requests without needing to run a server.

Fixtures

Django allows you to define fixtures, which are sets of initial data that can be loaded into the test database. This is useful for setting up the database in a known state before running tests.

Assertions

Django’s test framework includes many assertion methods (inherited from unittest) to verify that your code produces the expected results. For example, you can check the status code of a response, verify that an object was created in the database, or check if a template was rendered.

Testing Django-Specific Features:

Models

You can create, update, and delete model instances in the test database and check if they behave correctly.

Views

You can test views by sending simulated requests and checking the response (status code, content, etc.).

Forms

You can test if forms validate correctly and if errors are raised when data is incorrect.

Templates

You can test whether the correct templates are rendered and if the context data is accurate.

Comparison between SimpleTestCase and TestCase

Django provides two main classes for writing tests: TestCase and SimpleTestCase. Here’s a comparison of their features, use cases, and differences:

Feature TestCase SimpleTestCase
Purpose Used for testing Django applications with database interactions. Used for testing simple scenarios without database access.
Database Setup Creates a test database for each test, which is set up and torn down automatically. Does not interact with a database; no test database is created.
Performance Slower due to database setup and teardown overhead. Faster since it doesn’t require database interaction.
Use Cases Ideal for testing models, views, and forms that require database access. Suitable for testing utility functions, views that do not require database access, or simple assertions.
Transaction Management Each test runs in a transaction that is rolled back after the test completes, ensuring a clean state for each test. No transaction management since it doesn’t involve a database.
Fixtures Support Supports loading fixtures to populate the test database before tests run. Does not support fixtures since there is no database.
Assertions Provides access to all standard assertions along with additional ones relevant to database tests (e.g., assertQuerysetEqual). Provides standard assertions but does not include database-related assertions.

Set Up Procedure

  1. Prepare the Testing Structure
    Begin by removing the auto-generated test.py file from the app directory. Create a new directory named tests within the app folder.

  2. Organize the Tests
    Inside the tests directory, create an __init__.py file and separate test files for each component (e.g., test_urls.py for testing URLs). As an example,

    File structure of Django’s Built-in Test Framework

  3. Writing Test Functions

    Example for testing urls.py:

    from django.test import SimpleTestCase
    from django.urls import reverse, resolve
    from course_registration.views import home
    
    class TestUrls(SimpleTestCase):
        def test_home_url_is_resolved(self):
            url = reverse('home')
            resolved = resolve(url)
            self.assertEqual(resolved.func, home)
    

    Example for testing views.py:

    from django.test import TestCase, Client
    from django.urls import reverse
    from course_registration.models import Course
    
    class TestViews(TestCase):
        def setUp(self):
            self.client = Client()
            self.valid_course = {
                "course_id": "CSE-405",
                "course_name": "Digital Image Processing",
                "course_type": "Theory",
                "credit": 3.0,
                "tutorial_full_marks": 20.0,
                "att_full_marks": 10.0,
                "final_full_marks": 70.0,
            }
            self.invalid_course = {
                "course_id": "",
                "course_name": "Some Course",
                "course_type": "Theory",
                "credit": 3.0,
                "tutorial_full_marks": 20.0,
                "att_full_marks": 10.0,
                "final_full_marks": 70.0,
            }
    
        def test_course_list_GET(self):
            response = self.client.get(reverse("course_list"))
            self.assertEqual(response.status_code, 200)
            self.assertTemplateUsed(response, "courses.html")
    
        def test_course_save_POST_valid_data(self):
            response = self.client.post(reverse("course_save"), self.valid_course)
            self.assertEqual(Course.objects.count(), 1)
            course = Course.objects.first()
            self.assertEqual(course.course_id, "CSE-405")
            self.assertEqual(response.status_code, 302)
            self.assertRedirects(response, reverse("course_list"))
    
        def test_course_save_POST_invalid_data(self):
            response = self.client.post(reverse("course_save"), self.invalid_course)
            self.assertEqual(Course.objects.count(), 0)
            self.assertEqual(response.status_code, 302)
            self.assertRedirects(response, reverse("course_list"))
    
        def test_course_remove(self):
            course = Course.objects.create(
                course_id="CS101",
                course_name="Introduction to Computer Science",
                course_type="Theory",
                credit=3.0,
                tutorial_full_marks=20.0,
                att_full_marks=10.0,
                final_full_marks=70.0,
            )
            course_id = course.id
            response = self.client.post(reverse("remove_course", args=[course_id]))
            self.assertFalse(Course.objects.filter(id=course_id).exists())
            self.assertEqual(response.status_code, 302)
            self.assertRedirects(response, reverse("course_list"))
    

    Example for testing models.py:

    from django.test import TestCase
    from course_registration.models import Course, Student
    from django.core.exceptions import ValidationError
    
    class TestModels(TestCase):
        def setUp(self):
            self.student = Student.objects.create(
                class_roll=349,
                registration_no=2024101,
                name="Subarna Saha",
                session="2019-2020",
                hall_name="Rokeya Hall",
            )
    
        def test_course_creation(self):
            course = Course.objects.create(
                course_id="CSE-410",
                course_name="Mobile App Development Laboratory",
                course_type="Laboratory",
                credit=1.0,
                tutorial_full_marks=50.0,
                att_full_marks=10.0,
                final_full_marks=40.0,
            )
            course.student.add(self.student)
    
            self.assertEqual(course.course_id, "CSE-410")
            self.assertEqual(course.course_name, "Mobile App Development Laboratory")
            self.assertEqual(course.course_type, "Laboratory")
            self.assertEqual(course.credit, 1.00)
            self.assertEqual(course.tutorial_full_marks, 50.00)
            self.assertEqual(course.att_full_marks, 10.00)
            self.assertEqual(course.final_full_marks, 40.00)
            self.assertIn(self.student, course.student.all())
            self.assertEqual(str(course), "CSE-410: Mobile App Development Laboratory")
    
        def test_course_type_choices(self):
            valid_types = ["Theory", "Laboratory", "Project"]
            for course_type in valid_types:
                course = Course.objects.create(
                    course_id=f"CSE-{valid_types.index(course_type) + 402}",
                    course_name=f"Course {course_type}",
                    course_type=course_type,
                    credit=3.00,
                    tutorial_full_marks=20.00,
                    att_full_marks=10.00,
                    final_full_marks=70.00,
                )
                self.assertEqual(course.course_type, course_type)
    
            invalid_course = Course(
                course_id="CSE-499",
                course_name="Invalid Course Type",
                course_type="Seminar",  # Invalid choice
                credit=3.00,
                tutorial_full_marks=20.00,
                att_full_marks=10.00,
                final_full_marks=70.00,
            )
            with self.assertRaises(ValidationError):
                invalid_course.full_clean()  # This should raise a ValidationError
    
  4. Running the Tests
    To execute the tests, use the command:

    python manage.py test app_name.tests
    

    The image below illustrates how to run tests using Django's built-in test framework:

    Running tests using Django’s Built-in Test Framework

Advantages

  1. Integration with Django:
    Django’s test framework is tightly integrated with its ecosystem, making it easy to test Django applications, including models, views, forms, and templates. This integration allows for the use of Django-specific utilities, such as test clients and database management.

  2. Comprehensive Testing Tools:
    The framework provides a rich set of testing tools and assertions, including TestCase, SimpleTestCase, and specialized classes for testing models, views, and forms. This variety helps developers create robust and comprehensive test suites tailored to their application’s needs.

  3. Automatic Test Discovery:
    Django supports automatic test discovery, which means that any test files or classes following the naming conventions will be automatically found and executed. This feature simplifies the testing process by reducing the need for manual configuration.

Disadvantages

  1. Performance Overhead:
    Running tests using Django’s test framework can be slower than using lightweight testing frameworks, especially when the test suite grows large. The use of an in-memory database during testing can add some overhead, which may impact the speed of the test runs.

  2. Steeper Learning Curve:
    For newcomers to Django, the built-in test framework can initially seem complex, especially when integrating with other Django features and concepts. Understanding how to effectively use the framework’s features and structure tests can take time and effort.

  3. Limited to Django Applications:
    The framework is designed specifically for Django applications, which means it may not be as suitable for projects that use a microservices architecture or where Django is not the primary framework. In such cases, developers may prefer more generic testing tools or libraries that offer greater flexibility.

Pytest

Author

  • Md Hasan Al Mamun
  • Saud Al Nahian

Introduction

Pytest is a popular Python testing framework that is designed to make writing and running tests easier. It supports writing simple unit tests, complex functional tests, and even more advanced forms of testing such as integration and end-to-end testing. Pytest is compatible with Python’s standard unittest module but offers more robust features, including test discovery, fixtures, and parametrization. Its minimalist syntax helps developers write clean, maintainable tests, making it a top choice for Python projects of all sizes.

Feature

  • Simple Syntax: Pytest offers an easy and clean syntax. Test functions are written in a plain Python style, making them simple to create and read without the need for classes or specific setup methods.

  • Automatic Test Discovery: Pytest automatically discovers test files and functions based on the naming convention (i.e., functions prefixed with test_).

  • Fixtures: Pytest's fixtures are used for setting up and tearing down environments. They allow the reuse of code between different tests, making test preparation more structured.

  • Parametrization: With Pytest, you can run a single test with different sets of parameters using the @pytest.mark.parametrize decorator, enabling more extensive test coverage.

  • Plugins: Pytest supports a broad range of plugins that extend its functionality, such as pytest-cov for code coverage, pytest-django for Django projects, and pytest-xdist for parallel execution.

  • Detailed Assertion Introspection: Pytest provides informative error messages when assertions fail, allowing for quicker and easier debugging.

Set Up Procedure

Install pytest

pytest requires: Python 3.8+ or PyPy3.

Run the following command in your command line:

pip install -U pytest

Check that you installed the correct version:

$ pytest --version
pytest 8.3.3 

Create your first test

Create a new file called test_sample.py, containing a function, and a test:

# content of test_sample.py
def func(x):
    return x + 1


def test_answer():
    assert func(3) == 5

The test

$ pytest
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 1 item

test_sample.py F                                                     [100%]

================================= FAILURES =================================
_______________________________ test_answer ________________________________

    def test_answer():
>       assert func(3) == 5
E       assert 4 == 5
E        +  where 4 = func(3)

test_sample.py:6: AssertionError
========================= short test summary info ==========================
FAILED test_sample.py::test_answer - assert 4 == 5
============================ 1 failed in 0.12s =============================

Advantages

  • Minimalist Syntax: Pytest’s syntax is clean and requires less boilerplate code compared to other testing frameworks.

  • Powerful Fixtures: The fixture mechanism is flexible and encourages reusable, modular test setups.

  • Parametrization: Pytest makes it easy to write multiple test cases with different inputs and expected outputs by using test parametrization.

  • Rich Plugin Ecosystem: Pytest has a wide range of plugins that allow for extended functionality, such as parallel test execution, integration with other tools, and test coverage measurement.

  • Detailed and Helpful Reports: Pytest provides highly detailed error messages when tests fail, which is incredibly helpful during debugging.

  • Compatibility with Other Testing Frameworks: Pytest works seamlessly with unittest and other Python testing tools, allowing it to be integrated into existing projects easily.

Disadvantages

  • Learning Curve for Advanced Features: While Pytest is simple for writing basic tests, some of the more advanced features, such as fixtures and custom plugins, might be harder for beginners to grasp.

  • Complexity in Large Projects: As projects grow, organizing tests and fixtures across multiple files can become complex. Managing dependencies between fixtures may require careful design to avoid issues.

  • Performance in Large Test Suites: Running a large number of tests sequentially can slow down as test suites grow, though this can be mitigated using Pytest’s parallelization tools like pytest-xdist.

Decision about selection of Unit Testing Tool

After comparing Django’s built-in test framework and Pytest for our Django project, we chose to proceed with Django's built-in framework.

The main reason is its seamless integration with Django's ecosystem, which provides built-in support for features like test client, fixtures, and database handling. This framework is optimized for testing Django-specific components such as models, views, and forms, making it easier for us to implement and maintain tests. Additionally, since it comes with Django by default, it avoids the need to configure and maintain an external testing tool, streamlining the development process. While Pytest offers flexibility and powerful features, we prioritized the simplicity and native support provided by Django’s framework to keep our unit testing aligned with Django’s conventions.

References