updated Oct 27, 2019

Python unit testing: call the same test method with parameters by data decorator

Common situation: we want to cover many similar test cases that differ in one or several variables values only (input test data, results etc), without code duplication.

Tests with one data value

The test method should be without parameters, but we could place the code called several times into a separate function and call it from the test method:

class TestPrice(TestCase):

  def test_positive(self):
    check_price_valid(10)
    check_price_valid(0.99)
    
  def check_price_valid(self, price):
    result = PriceValidator().check(price)
    self.assertTrue(result)

But there's still extra method there. There's another way to do the same but with the less code:
from ddt import ddt, data
    
@ddt
class TestPrice(TestCase):

  @data(10, 0.99)
  def test_positive(self, price):
    result = PriceValidator().check(price)
    self.assertTrue(result)
How it works: ddt test class decorator creates several test methods instead each test method with the data decorator: as many as quantity of the data decorator parameters. So test_positive method in the example above is replaced with two methods: their names consist from the original method name and the parameter values string suffix.

Tests with many data values

Let's assume we want something like this:
class TestPrice(TestCase):

  def test_price_valid(self):
    check_price_valid(10, True)
    check_price_valid(0, False)
    
  def check_price_valid(self, price, expected_result):
    result = PriceValidator().check(price)
    self.assertEquals(expected_result, result)

We could rewrite with data decorator like this:
from ddt import ddt, data
    
@ddt
class TestPrice(TestCase):

  @data((10, True), (0, False))
  def test_price_valid(self, test_data):
    price, expected_result = test_data
    result = PriceValidator().check(price)
    self.assertEquals(expected_result, result)
or even better, changing the test method signature to many arguments with unpack decorator so it's clear from the method parameters what's the test data it depends on:
from ddt import ddt, data, unpack
    
@ddt
class TestPrice(TestCase):

  @data((10, True), (0, False))
  @unpack
  def test_price_valid(self, price, expected_result):
    result = PriceValidator().check(price)
    self.assertEquals(expected_result, result)