Pytest fixture parametrization

for fun and profit

czEść!

  • Mam na imię Michał

  • Zajmuję się backendem, protokołami, bazami danych, czasem embedded

  • Obecnie pracuję w Vatix

  • Lubię chodzić po górach i DIY

the EXAMPLE: API

@dataclass
class API:
  user: User

  def list_todos(self): ...

  def create_todo(self, name): ...

  def share_todo(self, list_uuid, user, permissions): ...

  def list_items(self, list_uuid): ...

  def create_item(self, list_uuid, name): ...

  def retrieve_item(self, list_uuid, item_uuid): ...

  def update_item(self, list_uuid, item_uuid, status: Status): ...

the example: data model

TodoList

TodoItem

Organisation

User

Team

unittest

class MyTestCase(TestCase):
  def setUp(self):
    # prepare everything
    
  def test_something(self):
    ...
    
  def test_something_else(self):
    ...
    
    
  def tearDown(self):
    # clean up after tests

unittest

class APITestCase(TestCase):
  def setUp(self):
    super().setUp()
    self.faker = Faker()
    self.my_organisation = Organisation(name=faker.company())
    self.my_team = Team(
      name=faker.color(),
      organisation=self.my_organisation
    )
    self.my_user = User(
      name=faker.name(),
      team=self.my_team
    )
    
    
class TestListTodos(APITestCase):
  def test_empty(self):
    self.assertListEqual(self.my_user.api.list_todos(), [])

unittest

class TestMyListMixin:
  def setUp(self):
    super().setUp()
    self.my_todo_list = TodoList(
      owner=self.my_user,
      name=self.faker.bs()
    )


class TestListOwner(TestMyListMixin, APITestCase):
  def test_owner(self):
    self.assertListEqual(
      self.my_user.api.list_todos(),
      [self.my_todo_list]
    )

unittest

class TestListMate(TestMyListMixin, APITestCase):
  def setUp(self):
    super().setUp()
      self.my_team_mate = User(
        name=self.faker.name(),
        team=self.my_team,
        role=Role.USER
     )

  def test_mate(self):
    self.assertListEqual(self.my_team_mate.api.list_todos(), [])

unittest

class TestListManager(TestMyListMixin, APITestCase):
  def setUp(self):
    super().setUp()
    self.my_team_manager = User(
      name=self.faker.name(),
      team=self.my_team,
      role=Role.MANAGER
    )

  def test_manager(self):
    self.assertListEqual(
      self.my_team_manager.api.list_todos(),
      [self.my_todo_list]
    )

unittest

class TestListAdmin(TestMyListMixin, APITestCase):
  def setUp(self):
    super().setUp()
    self.my_admin = User(
      name=self.faker.name(),
      team=Team(name="Admins", organisation=self.my_organisation),
      role=Role.ADMIN
    )

  def test_admin(self):
    self.assertListEqual(
      self.my_admin.api.list_todos(),
      [self.my_todo_list]
    )

unittest

class TestOtherTeamMixin(TestMyListMixin):
  def setUp(self):
    super().setUp()
    self.other_team = Team(
      name=self.faker.color_name(),
      organisation=self.my_organisation
    )
        
        
class TestOtherOrganisationMixin(TestMyListMixin):
  def setUp(self):
    super().setUp()
    self.other_organisation = Organisation(name=self.faker.company())
    self.other_organisation_team = Team(
      name=self.faker.color_name(),
      organisation=self.other_organisation
    )

pytest

@pytest.fixture
def something():
    # prepare
    yield "Something"
    # cleanup

@pytest.fixture
def something_else():
    # prepare
    yield "Something else"
    # cleanup

    
def test_something(something):
    assert something == "Something"
    
    
def test_something_else(something_else):
    assert something_else == "Something else"

pytest

@pytest.fixture
def my_organisation(faker: Faker):
    return Organisation(name=faker.company())


@pytest.fixture
def my_team(faker: Faker, my_organisation: Organisation):
    return Team(
        name=faker.color_name(),
        organisation=my_organisation
    )


@pytest.fixture
def my_user(faker: Faker, my_team: Team):
    return User(name=faker.name(), team=my_team, role=Role.USER)

    
def test_empty(my_user):
    assert my_user.api.list_todos() == []

pytest

@pytest.fixture
def my_todo_list(faker: Faker, my_user: User):
  return TodoList(owner=my_user, name=faker.bs())
  
  
def test_owner(my_user, my_todo_list):
  assert my_user.api.list_todos() == [my_todo_list]

pytest

@pytest.fixture
def my_team_mate(faker: Faker, my_team: Team):
  return User(name=faker.name(), team=my_team, role=Role.USER)


def test_mate(my_team_mate, my_todo_list):
  assert my_team_mate.api.list_todos() == []
  

@pytest.fixture
def my_team_manager(faker: Faker, my_team: Team):
    return User(name=faker.name(), team=my_team, role=Role.MANAGER)

  
def test_manager(my_team_manager, my_todo_list):
  assert my_team_manager.api.list_todos() == [my_todo_list]

pytest

@pytest.fixture
def other_organisation():
    return Organisation(...)


@pytest.fixture
def other_organisation_team(other_organisation: Organisation):
    return Team(..., organisation=other_organisation)


@pytest.fixture
def other_organisation_mate(other_organisation_team):
    return User(..., team=other_organisation_team, role=Role.USER)


@pytest.fixture
def other_organisation_manager(other_organisation_team: Team):
    return User(..., team=other_organisation_team, role=Role.MANAGER)

trimming roots

my org

my team

my user

my team mate

my team manager

other team

other org

other org team

other team mate

other team manager

other org mate

other org manager

other org admin

my org admin

trimming roots: role?

my org

my team

my user

other user manager | user

other team

other org

other org team

other team mate

other team manager

other org mate

other org manager

other org admin

my org admin

PARAMETRIZE

@pytest.mark.parametrize(
  'a,b,result',
  [
    (1,2,3),
    (3,4,7),
  ]
)
def test_addition(a, b, result):
  assert a + b == result

PARAMETRIZE: fixtures

@pytest.fixture
def result():
  return 42


@pytest.mark.parametrize(
  'a,b,result',
  [
    (1, 2, 3),
    (3, 4, 7),
  ]
)
def test_addition(a, b, result):
  assert a + b == result

PARAMETRIZE: indirect

@pytest.fixture
def result(request):
  param = getattr(request, 'param', 41)
  return param + 1


def test_42(result):
  assert result == 42

  
@pytest.mark.parametrize(
  'a,b,result',
  [
    (1, 2, 2),
    (3, 4, 6),
  ],
  indirect=['result']
)
def test_addition(a, b, result):
  assert a + b == result
  
  

PARAMETRIZE: ROLE

@pytest.fixture
def other_user(request, my_team):
    role = getattr(request, 'param', Role.USER)
    return User(..., team=my_team, role=role)

  
@pytest.mark.parametrize('other_user', [
    Role.USER
], indirect=['other_user'])
def test_mate(self, other_user, my_todo_list):
    assert other_user.api.list_todos() == []

    
@pytest.mark.parametrize('other_user', [
    Role.MANAGER
], indirect=['other_user'])
def test_manager(self, other_user, my_todo_list):
    assert other_user.api.list_todos() == [my_todo_list]

trimming roots: team?

my org

my team

my user

other user
manager | user

other team

other org

other org team

other org mate

other org manager

other org admin

my org admin

parametrize: team

@pytest.fixture
def other_user(request):
    team_or_fixture = getattr(request, 'param', my_team)
    
    if hasattr(team_or_fixture, '_pytestfixturefunction'):
    	team = request.getfixturevalue(team_fixture.__name__)
    else:
        team = team_or_fixture
        
    return User(..., team=team)
  
  
@pytest.mark.parametrize('other_user', [
    other_team,
], indirect=True)
def test_other_manager(self, other_user, my_todo_list):
    assert other_user.api.list_todos() == []

parametrize: team AND ROLE

@pytest.fixture
def other_user(request):
    param = getattr(request, 'param', {})
    
    role = param.get('role', Role.USER)
    team_or_fixture = param.get('team', my_team)
    
    if hasattr(team_or_fixture, '_pytestfixturefunction'):
    	team = request.getfixturevalue(team_fixture.__name__)
    else:
        team = team_or_fixture
        
    return User(..., team=team, role=role)
  
  
@pytest.mark.parametrize('other_user', [
    dict(team=other_team, role=Role.USER),
], indirect=True)
def test_other_manager(self, other_user, my_todo_list):
    assert other_user.api.list_todos() == []

trimming roots: after

my org

my team

other team

other org

my user

other user
manager | user

sugar: dataclass

@dataclass
class OtherUserParam:
  role: Role = field(default=Role.USER)
  team: Team = field(default_factory=my_team)  # ???

    
@pytest.fixture
def other_user(request):
    param = make_param_model(request, OtherUserParam)
    
    return User(..., team=param.team, role=param.role)
  
  
@pytest.mark.parametrize(
    'other_user',
    [
        dict(team=other_team, role=Role.MANAGER)
    ],
    indirect=True
)
def test_other_manager(self, other_user, my_todo_list):
    assert other_user.api.list_todos() == []

sugar: decorator

@dataclass
class UserParam:
    team: Team = field(default_factory=my_team)
    role: Role = Role.USER


@parametrized_fixture(UserParam)
def my_user(request) -> User:
    return User(
      ...,
      team=request.param.team,
      role=request.param.role
    )

trimming branches

@pytest.mark.parametrize('other_user', [
    dict(role=Role.USER)
], indirect=True)
def test_mate(self, other_user, my_todo_list):
    assert other_user.api.list_todos() == []

@pytest.mark.parametrize('other_user', [
    dict(role=Role.MANAGER)
], indirect=True)
def test_manager(self, other_user, my_todo_list):
    assert other_user.api.list_todos() == [my_todo_list]

@pytest.mark.parametrize('other_user', [
    dict(team=other_team, role=Role.MANAGER)
], indirect=True)
def test_other_manager(self, other_user, my_todo_list):
     assert other_user.api.list_todos() == []

parametrize result?

@pytest.mark.parametrize('other_user, result', [
  	(
      dict(role=Role.USER),
      []
    ),
    (
      dict(role=Role.MANAGER, team=other_team),
      []
    ),
    (
      dict(role=Role.MANAGER),
      ...  # ???
    ),
], indirect=['other_user'])
def test_other_user(self, other_user, result):
    assert other_user.api.list_todos() == result

parametrize: result

@pytest.fixture
def result(request):
    func_or_value = getattr(request, 'param', None)
    
    if not inspect.isfunction(func_or_value):
      return func_or_value
      
    try:
        signature = inspect.signature(func_or_value)
        parameters = signature.parameters.values()
    except ValueError:
        parameters = []

    kwargs = {}
    for parameter in parameters:
        try:
            kwargs[parameter.name] = \
                request.getfixturevalue(parameter.name)
        except pytest.FixtureLookupError:
            continue

    return func_or_value(**kwargs)

parametrize: result

@pytest.mark.parametrize('other_user, result', [
  	(
      dict(role=Role.MANAGER),
      lambda my_todo_list: [my_todo_list]
    ),
], indirect=['other_user', 'result'])
def test_other_user(self, other_user, result):
    assert other_user.api.list_todos() == result

sugar: decorator

@parametrized_fixture
def result(request):
  raise NotImplementedError

trimming branches: after

@pytest.mark.parametrize('other_user, result', [
  	(
      dict(role=Role.USER),
      []
    ),
  	(
      dict(role=Role.MANAGER, team=other_team),
      []
    ),
    (
      dict(role=Role.MANAGER),
      lambda my_todo_list: [my_todo_list]
    ),
  ], indirect=['other_user', 'result']
)
def test_other_user(self, other_user, result):
  assert other_user.api.list_todos() == result

glue: always indirect

def pytest_generate_tests(metafunc):
    # 
    # use pytest API to find parametrization and add indirect=[]
    # where parameters refer to our "magic" fixtures
    #
    # implementation is on github ;)

putting it all together

@pytest.mark.parametrize("other_user, result", [
        (
          my_user,
          lambda my_todo_list: [my_todo_list]
        ),
        (
          dict(role=Role.USER),
          []
        ),
        (
          dict(role=Role.MANAGER, team=other_team),
          []
        ),
        (
          dict(role=Role.MANAGER),
          lambda my_todo_list: [my_todo_list]
        ),
        (
          dict(role=Role.ADMIN),
          lambda my_todo_list: [my_todo_list]
        ),
    ],
)
def test_same_organisation(self, other_team, other_user, my_todo_list, result):
    assert other_user.api.list_todos() == result

putting it all together

@pytest.mark.parametrize("my_todo_list, other_user", [
    (
      dict(
        collaborators=lambda other_user: {other_user: {Permission.CREATE}}
      ),
      dict(
        role=Role.USER,
        team=lambda other_organisation: Team(organisation=other_organisation),
      ),
    ),
  ],
)
def test_share_across_organisations(self, other_user, my_todo_list):
  item = other_user.api.create_item(my_todo_list.uuid, 'Buy milk')

  assert item
  assert my_todo_list.todoitem_set == [item]

challenge: layers

@pytest.mark.parametrize("other_user", [
  dict(role=Role.USER)
])
class TestMates:
  def test_same_organisation(self, other_user):
    ...
    
    
  @pytest.mark.parametrize("other_user", [
    dict(organisation=other_organisation)
  ])
  def test_other_organisation(self, other_user):
    ...

ValueError: duplicate parametrization of 'other_user'

mrzechonek/fixture_parametrization

thank you

Fixture parametrization for fun and profit

By Michał Lowas-Rzechonek

Fixture parametrization for fun and profit

  • 25