Имитационные данные — это распространенный метод тестирования программного обеспечения, которое зависит от внешних источников данных, таких как базы данных, веб-службы или API. Имитация данных означает создание поддельных или смоделированных данных, которые имитируют реальные данные, но без фактического взаимодействия с внешним источником. Имитационные данные могут быть полезны для некоторых сценариев, таких как:

  • Изоляция тестируемого модуля от других компонентов или зависимостей, не имеющих отношения к тесту.
  • Ускорение выполнения теста за счет исключения сетевых вызовов или запросов к базе данных, которые могут быть медленными или ненадежными.
  • Управление вводом и выводом теста путем создания предсказуемых и непротиворечивых данных, которые можно легко проверить.

Однако фиктивные данные также имеют некоторые серьезные недостатки и риски, такие как:

  • Внесение ошибок или несоответствий между фиктивными данными и реальными данными, что может привести к ложноположительным или ложноотрицательным результатам теста.
  • Снижение охвата и достоверности теста за счет отказа от проверки фактического поведения и логики внешнего источника или взаимодействия с ним.
  • Увеличение обслуживания и сложности теста за счет необходимости дополнительного кода и логики для создания фиктивных данных и управления ими.

В этом сообщении блога я утверждаю, что имитация данных — плохая практика для тестирования программного обеспечения, и что вам следует избегать этого, насколько это возможно. Я также покажу вам несколько примеров того, как тестировать программное обеспечение, зависящее от внешних источников данных, без имитации данных, используя Python и PyTest.

Пример имитации данных

Допустим, у нас есть функция, которая вычисляет среднюю оценку товара на основе отзывов из интернет-магазина. Функция принимает идентификатор продукта в качестве входных данных и возвращает средний рейтинг в качестве выходных данных. Функция использует API для получения отзывов из интернет-магазина.

import requests

def get_average_rating(product_id):
    """
    Returns the average rating of a product based on the reviews from an online store.
    """
    url = f"https://online-store.com/api/reviews/{product_id}"
    response = requests.get(url)
    if response.status_code == 200:
        reviews = response.json()
        ratings = [review["rating"] for review in reviews]
        average_rating = sum(pratings) / len(ratings)
        return average_rating
    else:
        raise Exception(f"Failed to get reviews for product {product_id}")

Чтобы протестировать эту функцию, у нас может возникнуть соблазн смоделировать данные, возвращаемые API, с помощью такой библиотеки, как unittest.mock или pytest-mock. Например, мы могли бы написать такой тест:

import pytest
from pytest_mock import mocker
from average_rating import get_average_rating

def test_get_average_rating(mocker):
    # Arrange
    product_id = 123
    mock_reviews = [
        {"rating": 5, "comment": "Great product!"},
        {"rating": 4, "comment": "Good quality."},
        {"rating": 3, "comment": "Not bad."}
    ]
    expected_average_rating = 4.0
    mocker.patch("requests.get")
    requests.get.return_value.status_code = 200
    requests.get.return_value.json.return_value = mock_reviews

    # Act
    actual_average_rating = get_average_rating(product_id)

    # Assert
    assert actual_average_rating == expected_average_rating

Этот тест создает фиктивный объект для request.get и устанавливает его возвращаемое значение, чтобы имитировать успешный ответ от API с некоторыми поддельными отзывами. Затем он вызывает тестируемую функцию и утверждает, что она возвращает ожидаемый средний рейтинг.

Проблемы с фиктивными данными

На первый взгляд, этот тест может показаться хорошим. Он работает быстро, проходит и охватывает основную логику функции. Однако есть несколько проблем с этим тестом, которые делают его ненадежным и неэффективным.

Тест не проверяет реальные данные

Первая проблема заключается в том, что тест проверяет не реальные данные, поступающие от API, а только их смоделированную версию. Это означает, что между фиктивными данными и реальными данными могут быть различия или несоответствия, которые могут повлиять на поведение или результат функции.

Например, что, если настоящий API возвращает больше полей, чем просто рейтинг и комментарий? Что делать, если некоторые из этих полей требуются или используются функцией? Что, если имена или типы некоторых из этих полей отличаются от ожидаемых? Что делать, если некоторые из этих полей имеют недопустимые или неожиданные значения?

Все эти сценарии могут вызвать ошибки или ошибки в функции, которые не будут обнаружены тестом, поскольку он использует только упрощенную версию данных. Тест будет пройден, даже если функция не работает с реальными данными.

Тест не проверяет взаимодействие с внешним источником

Вторая проблема заключается в том, что тест проверяет не взаимодействие с внешним источником, а только его издевательскую версию. Это означает, что могут возникнуть проблемы или сбои при обмене данными или интеграции с API, которые могут повлиять на поведение или результат функции.

Например, что делать, если API не работает или недоступен? Что делать, если API работает медленно или не отвечает? Что делать, если API возвращает ошибку или код состояния, отличный от ожидаемого? Что, если API изменит свой формат или структуру без предварительного уведомления?

Все эти сценарии могут привести к ошибкам или ошибкам в функции, которые не будут обнаружены тестом, поскольку он использует только фиктивную версию ответа. Тест будет пройден, даже если функция не работает с реальными данными.

Тест сложно поддерживать и обновлять

Третья проблема заключается в том, что тест сложно поддерживать и обновлять, поскольку для создания фиктивных данных и управления ими требуется дополнительный код и логика. Это означает, что в самом тестовом коде могут быть ошибки или несоответствия, которые могут повлиять на надежность или достоверность теста.

Например, что, если фиктивные данные устарели или неверны? Что делать, если фиктивные данные неполные или отсутствуют некоторые случаи? Что делать, если фиктивные данные дублируются или несовместимы в разных тестах? Что делать, если фиктивные данные трудно читать или понимать?

Все эти сценарии могут привести к ошибкам или ошибкам в тестовом коде, которые повлияют на результаты теста. Тест провалится или пройдет по неверным причинам.

Как тестировать без фиктивных данных

Итак, как мы можем тестировать программное обеспечение, которое зависит от внешних источников данных, не имитируя данные? Существует несколько возможных решений в зависимости от контекста и характера источника данных. Вот несколько общих рекомендаций и советов:

  • По возможности используйте реальный или оперативный источник данных. Это лучший способ убедиться, что ваш тест проверяет реальные данные и реальное взаимодействие с внешним источником. Однако это не всегда возможно или желательно из-за таких факторов, как стоимость, доступность, безопасность или конфиденциальность.
  • По возможности используйте локальный источник данных или источник данных в памяти. Это хорошая альтернатива использованию реального или оперативного источника данных, поскольку он позволяет создавать собственные данные и управлять ими, не полагаясь на внешний источник. Однако для этого может потребоваться дополнительная настройка и настройка, и он может не всегда быть совместимым или несовместимым с внешним источником.
  • По возможности используйте поддельный или заглушенный источник данных. Это хорошая альтернатива использованию фиктивного источника данных, поскольку он позволяет имитировать реальный ответ из внешнего источника без создания поддельных данных. Однако для этого может потребоваться дополнительный код и логика, и это может быть не всегда точным или реалистичным.

Давайте посмотрим, как мы можем применить эти решения на нашем примере тестирования функции, которая вычисляет среднюю оценку товара на основе отзывов из интернет-магазина.

Использование реального источника данных

Один из вариантов — использовать реальный API интернет-магазина, предоставляющий обзоры товаров. Например, мы могли бы использовать API рекламы товаров Amazon, который позволяет нам искать товары и получать их рейтинги и обзоры. Чтобы использовать этот API, нам нужно зарегистрировать учетную запись и получить ключ доступа и секретный ключ.

Нам также нужно немного изменить нашу функцию, чтобы использовать этот API вместо поддельного:

import requests
from amazon.paapi import AmazonAPI

def get_average_rating(product_id):
    """Returns the average rating of a product based on the reviews from Amazon."""
    amazon = AmazonAPI(ACCESS_KEY, SECRET_KEY)
    product = amazon.get_product(product_id)
    reviews = product.reviews
    ratings = [review["rating"] for review in reviews]
    average_rating = sum(ratings) / len(ratings)
    return average_rating

Тогда мы могли бы написать такой тест:

import pytest
from average_rating import get_average_rating

def test_get_average_rating():
    # Arrange
    product_id = "B07XJ8C8F5" # A random product from Amazon
    expected_average_rating = 4.6 # The actual average rating from Amazon

    # Act
    actual_average_rating = get_average_rating(product_id)

    # Assert
    assert actual_average_rating == expected_average_rating

В этом тесте используется реальный идентификатор продукта от Amazon и проверяется, возвращает ли функция ту же среднюю оценку, что и на веб-сайте Amazon.

Преимущество этого теста в том, что он использует реальные данные и реальное взаимодействие с внешним источником, что увеличивает его охват и достоверность. Недостатком является то, что он зависит от внешнего источника, который может быть недоступен или ненадежен, что снижает его скорость и стабильность.

Использование локального источника данных

Другой вариант — использовать локальную базу данных, содержащую отзывы о продуктах. Например, мы могли бы использовать SQLite, легкий и автономный движок базы данных, который может работать в памяти. Чтобы использовать SQLite, нам нужно установить его и создать файл базы данных с некоторыми примерами данных.

Нам также нужно немного изменить нашу функцию, чтобы использовать SQLite вместо поддельного API:

import sqlite3

def get_average_rating(product_id):
    """Returns the average rating of a product based on the reviews from a local database."""
    conn = sqlite3.connect("reviews.db")
    cursor = conn.cursor()

    # Query the database for the reviews of the product
    query = "SELECT rating FROM reviews WHERE product_id = ?"
    cursor.execute(query, (product_id,))
    reviews = cursor.fetchall()

    # Calculate the average rating
    ratings = [review[0] for review in reviews]
    average_rating = sum(ratings) / len(ratings)

    # Close the connection
    conn.close()

    return average_rating

Тогда мы могли бы написать такой тест:

import pytest
from average_rating import get_average_rating

def test_get_average_rating():
    # Arrange
    product_id = 123 # A random product from the database
    expected_average_rating = 4.0 # The actual average rating from the database

    # Act
    actual_average_rating = get_average_rating(product_id)

    # Assert
    assert actual_average_rating == expected_average_rating

Этот тест использует реальный идентификатор продукта из базы данных и проверяет, возвращает ли функция ту же среднюю оценку, которая хранится в базе данных.

Преимущество этого теста в том, что он использует реальные данные и реальное взаимодействие с локальным источником, что повышает его скорость и стабильность. Недостатком является то, что он требует дополнительной настройки и настройки и может быть несовместим или несовместим с внешним источником.