Почему неправильно считается покрытие в коде с абстрактным классом / методом

Рейтинг: 0Ответов: 1Опубликовано: 23.06.2023

Вот так выглядит код, который я хочу протестировать:

class AbstractWrapper(ABC):
    @abstractmethod
    def func(self) -> None:
        raise NotImplementedError  # *


class Wrapper(AbstractWrapper):
    def func(self) -> None:
        print(1)  # *

Я написал pytest тесты и запустил команду pytest --cov --cov-report=html. Но почему-то в отчете покзаывает, что строки со * не покрыты тестами. Но тесты работают корректно и проверяют код (с неправильными входными данными тесты не проходят, с правильными - проходят).

Как это можно исправить, может я не ту команду использую?

Ответы

▲ 2Принят

Прежде всего, вы звездочкой отметили, что print(1) у вас показывает не покрытым, но у меня такая проблема не воспроизводится. Этот print фактически в процессе выполнения теста выполняется, и в покрытии он учитывается.

Далее, по поводу абстрактного метода. Если вы тестируете только Wrapper, то исходный не переопределенный метод останется не покрытым, т.к. фактически исходный метод не вызывается:

from abc import ABC, abstractmethod


class AbstractWrapper(ABC):
    @abstractmethod
    def func(self) -> None:
        raise NotImplementedError


class Wrapper(AbstractWrapper):
    def func(self) -> None:
        print(1)  # *


def test_wrapper(capsys):
    wrapper = Wrapper()
    wrapper.func()
    captured = capsys.readouterr()
    assert captured.out == "1\n"

В данном случае все пишу в одном файле test.py, тесты запускаю командой

pytest test.py --cov --cov-report=html

Покрытие:

введите сюда описание изображения

Нужно или сделать метод не абстрактным, чтобы можно было отнаследоваться и вызвать исходный вариант метода (проверить, что он действительно кидает NotImplementedError):

from abc import ABC
import pytest


class AbstractWrapper(ABC):
    def func(self) -> None:
        raise NotImplementedError


...


def test_abstract_wrapper():
    class Test(AbstractWrapper):
        pass

    wrapper = Test()
    with pytest.raises(NotImplementedError):
        wrapper.func()

Покрытие:

введите сюда описание изображения

Либо добавляете комментарий # pragma: no cover абстрактному методу, тогда покрытие по нему не будет учитываться:

введите сюда описание изображения

Также можно добавить декоратор абстрактного метода в exclude_also в конфиге .coveragerc (конфиг нужно положить в папку, откуда вызываете pytest) - это наиболее удобный вариант, на мой взгляд:

[report]
exclude_also =
    @(abc\.)?abstractmethod

Полный пример из документации модуля coverage.py (Excluding code from coverage.py / Advanced exclusion):

[report]
exclude_also =
    def __repr__
    if self.debug:
    if settings.DEBUG
    raise AssertionError
    raise NotImplementedError
    if 0:
    if __name__ == .__main__.:
    if TYPE_CHECKING:
    class .*\bProtocol\):
    @(abc\.)?abstractmethod

Результат:

введите сюда описание изображения