4

Basically, I realize that I am writing the same test case (test_update_with_only_1_field) for a similar URL for multiple models

from django.test import RequestFactory, TestCase
class BaseApiTest(TestCase):
def setUp(self):
    superuser = User.objects.create_superuser('test', 'test@api.com', 'testpassword')
    self.factory = RequestFactory()
    self.user = superuser
    self.client.login(username=superuser.username, password='testpassword')

class SomeModelApiTests(base_tests.BaseApiTest):
def test_update_with_only_1_field(self):
    """
    Tests for update only 1 field 

    GIVEN the following shape and related are valid
    WHEN we update only with just 1 field
    THEN we expect the update to be successful
    """
    shape_data = {
        'name': 'test shape',
        'name_en': 'test shape en',
        'name_zh_hans': 'test shape zh hans',
        'serial_number': 'test shape serial number',
        'model_name': {
            'some_field': '123'
        }
    }

    data = json.dumps(shape_data)
    response = self.client.post(reverse('shape-list-create'), data, 'application/json')
    self.assertEqual(response.status_code, status.HTTP_201_CREATED)

    some_model = response.data['some_model']
    new_some_field = '12345'

    data = json.dumps({'some_field': new_some_field, 'id': response.data['some_model']['id']})
    response = self.client.put(reverse('some-model', args=[some_model['id']]), data, 'application/json')
    self.assertEqual(response.status_code, status.HTTP_200_OK)
    self.assertEqual(new_some_field, response.data['some_field'])

I need to do this for more than 10 times. Which I have already done so.

the only difference each time, is the following phrases "some_model", "some-model", and "some_field"

I was wondering if there's a faster way to do this.

I can think abstractly two ways:

  1. create a template in a text editor that somehow can generate the final test case which I then copy and paste. I am using sublime text 3 though I am okay to switch to another text editor

  2. There's a way I can write slightly more code in the form of converting this test case into a behavior class that the individual test class can call. aka composition.

Which one makes more sense or there's a different way to do this?

Please note that BaseApi class is also inherited by other test class that do NOT have that repetitive test case method.

georgexsh
  • 15,984
  • 2
  • 37
  • 62
Kim Stacks
  • 10,202
  • 35
  • 151
  • 282

4 Answers4

5

I guess what you want is "parameterized tests", standard unittest could do this with parameterized package:

import unittest
from parameterized import parameterized

class SomeModelApiTests(unittest.TestCase):

    @parameterized.expand([
        ('case1', 'm1', 'f1', 'nf1'),
        ('case1', 'm2', 'f2', 'nf2'),
    ])
    def test_update_with_only_1_field(self, dummy_subtest_name, model_name, field_name, new_field_value):
        print(model_name, field_name, new_field_value)

will yields:

test_update_with_only_1_field_0_case1 (t.SomeModelApiTests) ... m1 f1 nf1
ok
test_update_with_only_1_field_1_case1 (t.SomeModelApiTests) ... m2 f2 nf2
ok

pytest testing framework has better support builtin on parameterized tests, worth looking at.

georgexsh
  • 15,984
  • 2
  • 37
  • 62
2

You could create a list / dict of "some_model" to test, and use subtest (https://docs.python.org/3/library/unittest.html#distinguishing-test-iterations-using-subtests) for each of your "some_model" items.

my_list_of_model = [FirstModel, SecondModel]

for my_model in my_list_of_model:
    with subTest(model=mymodel):
        # Testing model here

If you want a different TestCase for each of your model, I think the multiple inheritance is the way to go:

class BaseApiTestCase(TestCase):
    def setUp():
        # Setup stuff

class RepetitiveTestCaseMixin:
    # Class to do the repetitive stuff
    def test_update_should_work(self):
        # Do some thing with self.model and self.field here

class ModelTestCase(BaseApiTestCase, RepetitiveTestCaseMixin):
    @classmethod
    def setUpClass(cls):
       super().setUpClass()

       cls.model = MyModel
       cls.field = 'some_field'
Clément Denoix
  • 1,504
  • 11
  • 18
  • Sorry if I sound pedantic but isn’t your second solution more like multiple inheritance rather than composition? – Kim Stacks Nov 13 '17 at 15:46
  • And actually the baseapi class is inherited by other model test class that don’t have the repetitive test case. How would that change the second solution? – Kim Stacks Nov 13 '17 at 15:50
  • 1
    It's indeed multiple inheritance, I edited my answer. To answer your second comment, You should just move your `test_update_should_work` and other shared method in a mixin, not in the `BaseApiTestCase`, so your ModelTestCase will looks like (`class ModelTestCase(BaseTestCase, RepetitiveTestCaseMixin)`... So you will have the choice to inherit (or not) from the `RepetitiveTestCaseMixin` (Edited my answer in this way) – Clément Denoix Nov 13 '17 at 16:00
  • How do I pass in the varying value for ‘some-field’ into the mixing method? – Kim Stacks Nov 13 '17 at 16:16
  • Using class / instance attributes ? In my answer, you can see I define `cls.model` in the child class, and I access it in the `RepetitiveTestCaseMixin` (`# Do some thing with self.model here`). You can set multiples values as needed using this way (or better, just class attributes...) – Clément Denoix Nov 13 '17 at 16:21
  • Set up the classs attribute under the class Method setupClass? – Kim Stacks Nov 13 '17 at 16:24
  • Yes. as `cls.some_field = 'some-field'` and you will have access in the `test_update_should_work` as `self.some_field` – Clément Denoix Nov 13 '17 at 16:31
2

Projects I work on we sometimes use mixin + "customization hooks" when a test needs to repeated. (and endpoints like the "shape-list-create" is subject to change/refactored)

Example for question:

class TestUpdateWithOnly1FieldMixin(object):
    some_model = None
    some_field = None
    some_model2 = None

    def get_some_model(self):
        return self.some_model

    def get_some_field(self):
        return self.some_field

    def get_some_model2(self):
        return self.some_model2

    def test_update_with_only_1_field(self):
        some_model = self.get_some_model()
        # represents some-model in example
        some_model2 = self.get_some_model2()
        some_field = self.get_some_field()

        shape_data = {
            'name': 'test shape',
            'name_en': 'test shape en',
            'name_zh_hans': 'test shape zh hans',
            'serial_number': 'test shape serial number',
            'model_name': {
                some_field: '123'
            }
        }

      data = json.dumps(shape_data)
      response = self.client.post(reverse('shape-list-create'), data, 'application/json')
      self.assertEqual(response.status_code, status.HTTP_201_CREATED)

      some_model_data = response.data[some_model]



class SomeModelApiTests(base_tests.BaseApiTest, TestUpdateWithOnly1FieldMixin):
    some_model = 'choose your model'
    some_field = 'some_field'
    some_model2 = 'some-model'

    def get_some_field(self):
        # Do customization
        return 'some-field after customize'

How to split the customization hooks and what to put in mixin etc is based on the situation. In my opinion the goal is to have the actual test case easy to follow. (Maybe move the "post shape-list-create" into a separate function as it might not really be relevant for that test case)

Another example, going a bit overboard with customizations but just to give an idea.

class TestWithGoodNameMixin(object):
    some_model = None
    some_field = None

    # "Customization hooks"

    def get_shape_data(self):
        return {self.some_field: 'x'}

    def create_model(self, shape_data):
        response = self.client.post(reverse('shape-list-create'), shape_data,
                                    'application/json')
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        return response[self.some_model]

    def create_put_data(self, some_model_data):
        # Add default implementation
        pass

    # .....

    def test_update_with_only_1_field(self):
        shape_data = self.get_shape_data()
        some_model_data = self.create_model(shape_data)

        data = self.create_put_data(some_model_data)
        response = self.put_data(data)

        self.assert_put_response(response)
Daniel Backman
  • 5,121
  • 1
  • 32
  • 37
0

You can use pytest package for unit testing. It is very simple and easy to use.

@pytest.mark.parametrize() decorator can be used to achieve that functionality.

An example for parametrized test cases is as follows:

import pytest
class SampleTesting(object):
    data_for_test = [
                      ('{inputdata1:value1}','output1'),
                      ('{inputdata1:value2}','output2')
                     ]
   @pytest.mark.parametrized('input_data, expected_output', data_for_test)
   def test_sample_function(self, input_data, expected_output):
       response = function_to_be_tested(input_data)
       assert response == expected_output

You can read more about this decorator in the docs'

You can also use the @pytest.fixture() decorator to setup the test function.

veri_pudicha_coder
  • 1,411
  • 9
  • 9