Saturday, March 23, 2019

Django Tests

Testing is an essential part of programming. The idea of tests is to verify that all the parts of your application work as intended.

Django incorporates tests from the beginning. When it builds the initial application it includes the test.py file. We are going to use it to prepare some unit tests.

Our testing will involve these steps:

  1. Testing the models
  2. Testing the views
  3. Creating sample data for tests
  4. testing a view that takes an id for parameter
  5. Testing context elements in view

Testing Models

The empty test.py file has one import, the class "TestCase." All of our tests will inherit this class. To test our models we also need to import them.

from django.test import TestCase
from .models import Product, ProductType, Review

Test are methods in a Class that inherits from TestCase. Our first tests will be for ProductType. We will test that the __str__() function returns the type name as we specified and secondly that the meta class has the correct name for the table. Here are the tests:

class ProductTypeTest(TestCase):
   def test_string(self):
       type=ProductType(typename="Tablet")
       self.assertEqual(str(type), type.typename)

   def test_table(self):
       self.assertEqual(str(ProductType._meta.db_table), 'producttype')

In the test we create a quick instance of the ProductType class and assign a value to its name. It is important to realize that the tests do NOT read the data in the actual database. The tests create a local test database it uses for the tests. That means you have to create any objects you need locally.

The "assertEqual" is the crux of the test. It is saying that we assert that the results of casting an instance of ProductType to string will produce the typename. (That IS the purpose of the __str__() function.) If it is True, the test will pass, otherwise fail.

There are actually 3 results any test can have, "Pass," "Fail," or "Error." Fail means that the test was conducted properly but the result was not what was asserted. (You should note that you can assert many things other than that two values are equal.) Error, means there is something wrong with the test itself, a syntax or logical error.

The ProductTest class has the same two tests for string and table name, but it also has two other tests. It tests the discount method and it tests the product type associated with it. To do this we create a setup function before the actual tests. The setup function establishes an instance of a type and a product that can be used in the other tests.

You should run each test as you write it. Save the file and key in this command at the command line:

python manage.py test -v 2

The "-v 2" tells it to run in the "verbose" mode which provides much more information about the tests.

Here is the complete ProductTest class

class ProductTest(TestCase):
   #set up one time sample data
   def setup(self):
       type = ProductType(typename='laptop')
       product=Product(productname='Lenovo', producttype=type, productprice='500.00')
       return product
   def test_string(self):
       prod = self.setup()
       self.assertEqual(str(prod), prod.productname)
  
   #test the discount property
   def test_discount(self):
       prod=self.setup()
       self.assertEqual(prod.memberdiscount(), 25.00)

   def test_type(self):
       prod=self.setup()
       self.assertEqual(str(prod.producttype), 'laptop')

   def test_table(self):
       self.assertEqual(str(Product._meta.db_table), 'product')

One point of advice on testing the discount or similar calculated values: Choose a value where you can easily determine if the calculation is correct or not.

For the Review Model, we basically ran the same tests as for the ProductType.

class ReviewTest(TestCase):
   def test_string(self):
       rev=Review(reviewtitle="Best Review")
       self.assertEqual(str(rev), rev.reviewtitle)

   def test_table(self):
       self.assertEqual(str(Review._meta.db_table), 'review')

Testing Basic views

We need to add a couple of imports for Views.

from .views import index, gettypes, getproducts
from django.urls import reverse
from django.contrib.auth.models import User

For both the index view and getproducts view we are only going to test that they are accessible by name. This is where the "reverse" that we just imported becomes necessary. Status code 200 means that all is good. There are other status codes that could apply. There are many other tests that could be done, but I will leave these as sufficient for now. Here are both tests:

class IndexTest(TestCase):
   def test_view_url_accessible_by_name(self):
       response = self.client.get(reverse('index'))
       self.assertEqual(response.status_code, 200)
  
class GetProductsTest(TestCase):
   def test_view_url_accessible_by_name(self):
       response = self.client.get(reverse('products'))
       self.assertEqual(response.status_code, 200)

Creating Sample Data

The product detail class will be much more interesting. For one thing it takes the parameter of a product ID. Also we added several things to the context such as the discount and the count of reviews. Remember Django tests do not use the actual database, so we need to create the sample data that we want to use.

We need a product which requires a product type and a user, and we want reviews. Here is the setup function for the class:

def setUp(self):
        self.u=User.objects.create(username='myuser')
        self.type=ProductType.objects.create(typename='laptop')
        self.prod = Product.objects.create(productname='product1', producttype=self.type, user=self.u, productprice=500, productentrydate='2019-04-02', productdescription="a product")
        self.rev1=Review.objects.create(reviewtitle='prodreview', reviewdate='2019-04-03', product=self.prod, reviewrating=4, reviewtext='some review')
        self.rev1.user.add(self.u)
        self.rev2=Review.objects.create(reviewtitle='prodreview', reviewdate='2019-04-03', product=self.prod,  reviewrating=4, reviewtext='some review')
        self.rev2.user.add(self.u)

The user has to be added to the Review objects separately because of the many-to-many relationship.

Testing View with id as Parameter

After the setup, the first test again tests whether it can be located by name, but the productdetail takes an id as argument. We need to provide it with a value for that argument, which we can get from our sample data.

def test_product_detail_success(self):
        response = self.client.get(reverse('productdetails', args=(self.prod.id,)))
        # Assert that self.post is actually returned by the post_detail view
        self.assertEqual(response.status_code, 200)

Testing context elements

The last two tests, test the discount and the count of related reviews. Both again use the sample data in the setup.

def test_discount(self):
        discount=self.prod.memberdiscount()
        self.assertEqual(discount, 25.00)

    def test_number_of_reviews(self):
        reviews=Review.objects.filter(product=self.prod).count()
        self.assertEqual(reviews, 2)
        

When I run all the tests, I get this response:

System check identified no issues (0 silenced).
test_view_url_accessible_by_name (techapp.tests.GetProductsTest) ... ok
test_view_url_accessible_by_name (techapp.tests.IndexTest) ... ok
test_discount (techapp.tests.ProductDetailsTest) ... ok
test_number_of_reviews (techapp.tests.ProductDetailsTest) ... ok
test_product_detail_success (techapp.tests.ProductDetailsTest) ... ok
test_discount (techapp.tests.ProductTest) ... ok
test_string (techapp.tests.ProductTest) ... ok
test_table (techapp.tests.ProductTest) ... ok
test_type (techapp.tests.ProductTest) ... ok
test_string (techapp.tests.ProductTypeTest) ... ok
test_table (techapp.tests.ProductTypeTest) ... ok
test_string (techapp.tests.ReviewTest) ... ok
test_table (techapp.tests.ReviewTest) ... ok

previous next

No comments:

Post a Comment