Pytest monkeypatching функция, использующая многопроцессорность (через concurrent.futures.ProcessPoolExecutor), не работает должным образом. Если одна и та же функция написана с использованием одного процесса или многопоточности (через concurrent.futures.ThreadPoolExecutor),monkeypatch работает должным образом.
Почему не работает многопроцессорная обезьянка и как правильно обезьянь патчить многопроцессную функцию для тестирования?
Простейший пример кода, иллюстрирующий мой вопрос, приведен ниже. В реальных условиях я бы попытался обезвредить функцию, импортированную из другого модуля.
# file_a.py
import concurrent.futures as ccf
MY_CONSTANT = "hello"
def my_function():
return MY_CONSTANT
def singleprocess_f():
result = []
for _ in range(3):
result.append(my_function())
return result
def multithread_f():
result = []
with ccf.ThreadPoolExecutor() as executor:
futures = []
for _ in range(3):
future = executor.submit(my_function)
futures.append(future)
for future in ccf.as_completed(futures):
result.append(future.result())
return result
def multiprocess_f():
result = []
with ccf.ProcessPoolExecutor() as executor:
futures = []
for _ in range(3):
future = executor.submit(my_function)
futures.append(future)
for future in ccf.as_completed(futures):
result.append(future.result())
return result
Я ожидал, что все тесты пройдут успешно (pip install -U pytest, запустите тесты с помощью pytest test_file_a.py):
# test_file_a.py
from file_a import multiprocess_f, multithread_f, singleprocess_f
# PASSES:
def test_singleprocess_f(monkeypatch):
monkeypatch.setattr("file_a.MY_CONSTANT", "world")
result = singleprocess_f()
assert result == ["world"] * 3
# PASSES:
def test_multithread_f(monkeypatch):
monkeypatch.setattr("file_a.MY_CONSTANT", "world")
result = multithread_f()
assert result == ["world"] * 3
# FAILS:
def test_multiprocess_f(monkeypatch):
monkeypatch.setattr("file_a.MY_CONSTANT", "world")
result = multiprocess_f()
assert result == ["world"] * 3
@Booboo Уточнил, что Monkeypatch — это приспособление pytest, спасибо, что заметили это. Работает на macOS, но было бы идеально, если бы тесты не зависели от операционной системы.
Хорошо знать. Я обновил свой ответ, указав подход, который я бы выбрал.






Я предполагаю, что вы работаете на какой-то платформе, которая по умолчанию использует метод spawn для создания новых процессов (например, Windows). В этом случае не имеет значения, какое значение вы присвоите MY_CONSTANT в основном процессе, используя Monkeypatch или иным образом, поскольку при создании процессов дочернего пула они инициализируют свою память, повторно выполняя оператор MY_CONSTANT = "hello". То есть дочерние процессы, созданные с помощью spawn, не наследуют никаких значений от родительского процесса, а вместо этого инициализируют память, выполняя все операторы в глобальной области видимости (например, оператор импорта, объявления функций и переменных и т. д.).
Если это так, то вам необходимо предоставить функцию инициализатора пула, используя аргумент инициализатора при создании экземпляра ProcessPoolExecutor. Функция multiprocessing_f должна участвовать в тестировании, принимая необязательный аргумент исполнителя (многопроцессорный пул), который будет использоваться для тестирования:
Файл file_a.py
import concurrent.futures as ccf
MY_CONSTANT = "hello"
def my_function():
return MY_CONSTANT
def singleprocess_f():
result = []
for _ in range(3):
result.append(my_function())
return result
def multithread_f():
result = []
with ccf.ThreadPoolExecutor() as executor:
futures = []
for _ in range(3):
future = executor.submit(my_function)
futures.append(future)
for future in ccf.as_completed(futures):
result.append(future.result())
return result
def multiprocess_f(executor=None):
"""To allow unit testing, we may be passed an executor
to use."""
if executor is None:
executor = ccf.ProcessPoolExecutor()
result = []
with executor:
futures = []
for _ in range(3):
future = executor.submit(my_function)
futures.append(future)
for future in ccf.as_completed(futures):
result.append(future.result())
return result
Файл test_file_a.py
import concurrent.futures as ccf
import file_a
def init_pool_processes():
file_a.MY_CONSTANT = "world"
# PASSES:
def test_singleprocess_f(monkeypatch):
with monkeypatch.context() as m:
monkeypatch.setattr(file_a, "MY_CONSTANT", "world")
result = file_a.singleprocess_f()
assert result == ["world"] * 3
# PASSES:
def test_multithread_f(monkeypatch):
monkeypatch.setattr(file_a, "MY_CONSTANT", "world")
result = file_a.multithread_f()
assert result == ["world"] * 3
# PASSES:
def test_multiprocess_f():
# Special executor for testing:
executor = ccf.ProcessPoolExecutor(initializer=init_pool_processes)
result = file_a.multiprocess_f(executor)
assert result == ["world"] * 3
Что такое
monkeypatch? См. Как создать минимальный воспроизводимый пример. И на какой платформе вы работаете? Тот, который использует спавн?