Search code examples
djangofactory-boy

Better OOP practise to add methods to class with relational mapping


Assuming the below sample code structure

# Returns a region
class RegionFactory:
      .....

# Returns a user
class UserFactory():
      .....

# Assigns a user with a permission against specific region
class RegionUserPermissionFactory():
      user = UserFactory()
      permission = PermissionFactory()
      .... 

Now I would like to have a method that allows assigning "user region permission" and I think this method is much suited in the UserFactory because it resembles a real world-class template.

 def add_user_domain_permission(permission_options, domain):
     ....
     (Invokes RegionUserPermissionFactory to create objects relevant to
      the user)
     ....

Edited 25/09/2019 Is it a better approach to include methods related to RegionUserPermission in the parent class? In my current codebase, there are a few more classes that include user object and is it better OOP practise to add them to UserFactory? Does it violate abstraction encapsulation concepts?


Solution

  • The idea of factory_boy is to define factories as relevant to your test cases; the set of attributes and parameters should be defined to provide a simple API when writing tests. The library focuses on providing readable and maintainable tests, rather than adhering strictly to OOP principles ;)

    In your example, if some tests require a simple user and others want to attach it to a specific region, you could add a second factory dedicated to that use case, say a RegionalizedUserFactory:

    class RegionalizedUserFactory(UserFactory):
        class Params:
            # Tests would call RegionalizedUserFactory(region_code='xxx')
            # Setting this as a `Param` means that the `region_code` field won't be passed
            # to User.objects.create(...)
            region_code = 'asia'
    
        add_user_permission = factory.RelatedFactory(
            # Once the `User` is created, call UserPermissionFactory(user=the_user, ...)
            UserPermissionFactory, 'user',
    
            # And use this factory's region_code attribute as the code for RegionFactory
            permission__region__code=factory.SelfAttribute('....region_code'),
        )
    

    This would be used as follows:

    >>> UserFactory()  # No region perms
    <User: John Doe, perms=[]>
    >>> RegionalizedUserFactory()   # Default on asia
    <User: John Doe, perms=[<Permission: level=admin, region=<Region: asia>>]>
    >>> RegionalizedUserFactory(region_code='mars')   # Custom region
    <User: John Doe, perms=[<Permission: level=admin, region=<Region: mars>>]>
    

    You could also decide to merge both classes in a single declaration, using factory.Maybe (which conditionally enables declarations based on other fields):

    class UserFactory(factory.django.DjangoModelFactory):
        class Meta:
            model = User
    
        class Params:
            region_code = None
    
        add_user_permission = factory.Maybe(
            'region_code',
            factory.RelatedFactory(
                UserRegionPermission, 'user',
                permission__region__code=factory.SelfAttribute('....region'),
            ),
        )
    

    With that second factory:

    >>> UserFactory()  # No region_code => no perms
    <User: John Doe, perms=[]>
    >>> UserFactory(region_code='asia')
    <User: John Doe, perms=[<Permission: level=admin, region=<Region: asia>>]>