# Find duplicate tests in your suite

> Tests run twice when a test class is imported into another module or a parametrize case repeats another, and a copied test often doesn't test what its name says. How to find and fix them.

Every suite collects some tests that add nothing: the same test running twice, or a copy of a test that checks the same thing under another name. They cost time on every run, and the copies are worse than wasted time: a test named `test_model_copy` that never calls `model_copy()` tells everyone that `model_copy()` is tested when it isn't.

Three kinds are worth finding. The first two are exact: the duplicate is the same test. The third is usually a bug in the test.

## A test class imported into another module runs there too

pytest collects every class whose name starts with `Test` in a test module's namespace, and every function whose name starts with `test`, including the ones it imports:

```python
# tests/test_api.py
from tests.test_orders import TestOrders  # to reuse a helper method


def test_api_lists_orders(client): ...
```

Now every test in `TestOrders` runs twice, once as `tests/test_orders.py::TestOrders::test_total` and once as `tests/test_api.py::TestOrders::test_total`. Nothing fails, so nobody notices.

Import the module instead of the class (`from tests import test_orders`), or move what you share to a helper module or `conftest.py`. The plain `unittest` runner does the same with imported `TestCase` classes; Django's test runner removes the repeats.

## Parametrize cases with the same values

```python
@pytest.mark.parametrize("currency", ["eur", "usd", "gbp", "eur"])
def test_price(currency): ...
```

pytest gives the repeated value its own id (`eur0`, `eur1`) and runs it again. In a short list it's easy to spot. In a list built from a fixture file or a long table of cases, it isn't. Drop the repeated case.

## Tests with the same code as another test

A test copied to start a new one, and never changed to test the new thing:

```python
def test_dunder_copy():
    model = Model(a=1)
    copied = model.__copy__()
    assert copied == model


def test_model_copy():
    """model_copy() returns a copy."""
    model = Model(a=1)
    other = model.__copy__()  # should be model.model_copy()
    assert other == model
```

Both tests pass, and `model_copy()` is untested. pydantic's suite had a test just like this one. Django's suite has a few more, such as a test of localized time fields that never localizes the field, and a `test_equals` that asserts `.contains()`.

The fix is to make each copy test what its name says (the method, input or setting it's named after), or to delete it if the other test already covers that.

Some copies are on purpose. Django runs `test_request_after_client_2` with the same code as `test_request`, to check that nothing the previous test did leaks into the next one. Keep those, and leave a comment that says why.

## Inherited tests run in every subclass

```python
class CheckoutChecks(TestCase):
    def test_total(self): ...
    def test_tax(self): ...


class CardCheckoutTest(CheckoutChecks): ...


class InvoiceCheckoutTest(CheckoutChecks): ...
```

Each subclass runs every test it inherits, and the base class runs them too. That's often the point: the same tests against another backend or configuration. When a test doesn't depend on anything the subclasses change, it runs once per class for nothing. Move those tests to a class of their own. A base class that only exists to be inherited shouldn't run on its own: give it a name pytest doesn't collect (no `Test` prefix) or set `__test__ = False` on it, or make it a mixin that doesn't inherit from `TestCase`.

## What isn't a duplicate

- **Tests that differ only in their data.** `test_search_exact` and `test_search_startswith` can share every line but the lookup. Those are two scenarios, not one.
- **Tests that cover the same lines.** Two tests can run exactly the same code and check different things: one asserts the count, the other the error message. In a study of 18 projects, suites cut down to the tests that kept the same statement coverage missed up to 20% of the injected bugs (mutants) that the full suites caught. Don't delete a test because another one covers its lines.

## How flakytest finds them

flakytest finds the first two kinds when pytest collects the tests: a test function of the same class collected under a second node id, and parametrize cases whose values are equal (numbers, strings, `None`, and lists, tuples and dicts of them). It reports both as **Tests run twice**, with the time the repeats take.

It finds copies from your test files' source when it builds the report. Two tests count as copies only when they have the same code once their local variable names are ignored, and when everything that could make them behave differently is the same too: decorators and parametrize data, the fixtures they ask for, their class and their module's setup, and what every name they use refers to. It reports them as **Tests have the same code as another test**. On Django's 18,000 tests that takes about 3 seconds and finds 16 tests.

Inherited tests come from the class each test method is defined in, and flakytest reports them as **Test classes run the tests they inherit again**.

In flakytest.dev, a test that repeats another one has a badge in the run's list of tests, and its page says which test it repeats.

## Further reading

- [pytest: conventions for test discovery](https://docs.pytest.org/en/stable/explanation/goodpractices.html#conventions-for-python-test-discovery)
- [pytest: parametrizing tests](https://docs.pytest.org/en/stable/how-to/parametrize.html)
- [Shi et al., Balancing Trade-offs in Test-Suite Reduction (FSE 2014)](https://mir.cs.illinois.edu/gyori/pubs/fse14reduction.pdf)
