What are the best practices for managing test variables in `pytest` compared to `unittest`?

What are the best practices for managing test variables in pytest compared to unittest?

In unittest, I can set up variables in a class, and then the methods of this class can choose whichever variable they want to use. Here’s an example:

class TestClass(unittest.TestCase):
    def setUp(self):        
        self.varA = 1
        self.varB = 2
        self.varC = 3
        self.modified_varA = 2

    def test_1(self):
        do_something_with(self.varA, self.varB)

    def test_2(self):
        do_something_with(self.modified_varA, self.varC)

With unittest, it’s easy to group tests together in one class and use various variables across different methods. In pytest, however, I’m using fixtures in conftest.py instead of a class, like this:

@pytest.fixture(scope="module")
def input1():
    varA = 1
    varB = 2
    return varA, varB

@pytest.fixture(scope="module")
def input2():
    varA = 2
    varC = 3
    return varA, varC

I then feed these fixtures to my functions in a different file (let’s say test_this.py) for different test functions. But, I’m running into a couple of challenges:

  1. Since I can’t just declare local variables in conftest.py and directly import them, is there a better way to declare different variables in conftest.py that can be used in multiple functions in test_this.py? I have five different configurations for these variables, and defining that many different fixtures sounds cumbersome. I would rather go back to the unittest class structure, where I can define my variables and pick and choose what I want.

  2. Should I declare global variables in test_this.py and use them across functions? That doesn’t seem very Pythonic since these variables are only used in that file.

  3. Let’s say I have multiple test files, such as test_that.py and test_them.py. If I have shared variables between these files, how should I manage them? Should I create a file like variables.py in the same directory and import it where needed? This way, I can keep all data separate from the test logic.

  4. Does pytest discourage organizing tests using classes? Every example I read online uses a bunch of functions with fixtures only. What is the recommended way to define a class and methods for organizing tests in pytest?

  5. In a scenario where I need to use the result of one function in another, how would I do that in pytest? Since I have an assert statement at the end of a function instead of a return, I won’t be able to use this function as a fixture. How can I accomplish this? I understand that it’s not ideal for one test to rely on another, but is there a workaround?

In summary, what are the differences between pytest vs unittest in terms of managing variables and organizing tests? How can I effectively use the fixtures in pytest while keeping things flexible like I did with the unittest class structure?

In pytest, you can declare fixtures not only in conftest.py, but also in any Python module you wish, and then simply import that module wherever needed. Additionally, you can use fixtures similarly to how you use the setUp method in unittest.

Here’s an example:

@pytest.fixture(scope='class')
def input(request):
    request.cls.varA = 1
    request.cls.varB = 2
    request.cls.varC = 3
    request.cls.modified_varA = 2

@pytest.usefixtures('input')
class TestClass:
    def test_1(self):
        do_something_with(self.varA, self.varB)

    def test_2(self):
        do_something_with(self.modified_varA, self.varC)

Alternatively, you can define separate fixtures for each variable:

@pytest.fixture()
def fixture_a():
    return 1  # varA

@pytest.fixture()
def fixture_b():
    return 2  # varB

@pytest.fixture()
def fixture_c():
    return 3  # varC

@pytest.fixture()
def fixture_mod_A():
    return 2  # modified_varA

Or, you can create one fixture that returns all the variables together:

@pytest.fixture()
def all_variables():
    return {'varA': 1, 'varB': 2, 'varC': 3, 'modified_varA': 2}

If you have many configurations, you can even use an indirect parametrized fixture to return variables by your choice, though this can be a bit more complex:

@pytest.fixture()
def parametrized_input(request):
   vars = {'varA': 1, 'varB': 2, 'varC': 3}
   var_names = request.param
   return (vars[var_name] for var_name in var_names)

@pytest.mark.parametrize('parametrized_input', [('varA', 'varC')], indirect=True)
def test_1(parametrized_input):
    varA, varC = parametrized_input
    # your test logic here

Alternatively, you could create a fixture factory that generates fixtures dynamically. While this is an overkill for a small number of tests, it can be useful if you have many test cases with different variable configurations.

If you want to manage shared variables across multiple test files, you can create a separate variables.py file and import them in the test files that need them.

However, I recommend not directly importing this file into your test files but instead using a command-line option to specify which file to import. This approach allows you to choose different variable files without modifying your code, keeping your test configuration flexible.

Regarding the organization of tests, it’s important to note that pytest doesn’t discourage using classes for organizing tests.

You can still use classes in pytest, especially if you’re migrating from other frameworks like unittest or nose. I use classes in my tests because I migrated from nosetest, and I haven’t encountered any issues with using classes in pytest.

In situations where you need to use the result of one function in another, you can follow this approach:

def some_actions(a, b):
    # perform actions here
    result = a + b  # just an example action
    return result

def test():
    result = some_actions(1, 2)
    assert result == 3
@pytest.fixture()
def some_fixture():
    return some_actions(1, 2)

In this solution, the logic of the function is reusable as both a fixture and in individual tests. This avoids directly relying on another test, which is generally not a good practice.

Use Fixture for Shared Setup Between Tests: Instead of writing separate functions to handle logic, you could leverage a fixture that sets up shared variables and can be used across multiple tests in different modules.

This allows for cleaner and more manageable tests without code duplication.

Modularize Variables Across Files: As an alternative, you can organize your test setup into several files to ensure that shared variables and configurations are easily imported and used across multiple test cases in different test files.

This is particularly useful for large projects with many tests that need shared configurations. You can manage the variables in a centralized location like variables.py and use them via fixtures in your test files.