Как издеваться над random.choice в python?

#python #unit-testing #mocking

#python #модульное тестирование #издевательство

Вопрос:

Я хочу choice возвращать одно и то же значение 1000 каждый раз в моем unittest. Следующий код не работает.

 import unittest
from random import choice

from mock import mock

def a():
    return choice([1, 2, 3])

class mockobj(object):
    @classmethod
    def choice(cls, li):
        return 1000

class testMock(unittest.TestCase):

    def test1(self):
        with mock.patch('random.choice', mockobj.choice):
            self.assertEqual(a(), 1000)
  

Сообщение об ошибке выглядит следующим образом:

 Failure
Traceback (most recent call last):
  File "test.py", line 15, in test1
    self.assertEqual(a(), 1000)
AssertionError: 3 != 1000
  

Как я должен изменить его, чтобы заставить его работать? Я использую python2.7

Комментарии:

1. Что это за платформа тестирования python? Я не знаю о «mockobj». Почему этот код не работает — что он делает?

2. @DannyStaple Извините за неудобства. mockobj это просто класс, который я создал, я добавил его в код, чтобы вы могли просто запустить его, чтобы воспроизвести проблему.

Ответ №1:

Проблема здесь в том, что a() используется не исправленная версия random.choice .

Сравните функции a и b :

 import random
from random import choice

def a():
    return choice([1, 2, 3])

def b():
    return random.choice([1, 2, 3])

def choice1000(values):
    return 1000

import unittest.mock as mock

with mock.patch('random.choice', choice1000):
    print('a', a())
    print('b', b())
  

Он печатает, например:

 a 3
b 1000
  

Почему?

Проблема в этой строке:

 from random import choice
  

Он импортируется random , а затем сохраняется random.choice в новой переменной с именем choice .

Позже mock.patch исправил оригинал random.choice , но не локальный choice .

Могу ли я исправить локальный? ДА:

 with mock.patch('__main__.choice', choice1000):
    print('a', a())
    print('b', b())
  

Теперь он печатает, например

 a 1000
b 1
  

(Я использовал '__main__' , потому что я поместил этот код в файл верхнего уровня — в вашем случае это может быть что-то другое)

Итак, что делать?

Либо исправьте все, либо используйте другой подход. Например, patch a() вместо choice() .

Альтернативное решение

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

 def a():
    return random.choice([1, 2, 3, 1000])

def test1(self):
    random.seed(0)
    self.assertEqual(a(), 1000)
  

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

В последнем примере выше я тестировал a() after random.seed(0) once, и он вернул 1000, поэтому я могу быть уверен, что он будет делать это каждый раз:

 >>> import random
>>> random.seed(0)
>>> print (random.choice([1, 2, 3, 1000]))
1000
>>> random.seed(0)
>>> print (random.choice([1, 2, 3, 1000]))
1000
>>> random.seed(0)
>>> print (random.choice([1, 2, 3, 1000]))
1000
>>> random.seed(0)
>>> print (random.choice([1, 2, 3, 1000]))
1000
  

Ответ №2:

Я не знаю, что такое mockobj из тестов, но то, что вы можете сделать, это.

     @mock.patch('random.choice')
    def test1(self, choice_mock):
        choice_mock.return_value = 1000
        self.assertEqual(a(), 1000)
  

Ответ №3:

Я хотел бы улучшить ответ @Alex с помощью полного сценария, чтобы лучше понимать и быть адаптируемым к другим случаям.

 import random
from unittest import TestCase, mock

letters = ['A', 'B', 'C', 'D']

def get_random_words(): # Simple function using choice
  l = []
  for _ in range(3):
      l.append(random.choice(letters))
    
  return "".join(l)

class TestRandom(TestCase):

   @mock.patch('random.choice') # *(1)
   def test_get_random_words(self, mock_choice):
    
      mock_choice.side_effect = ['A','b','D','Z'] # *(2)
      result = get_random_words()
    
      self.assertEqual(result, 'AbD', 'Does not generate correct string')
  

Соображения

* (1) Для этого примера функция находится внутри того же файла, но в случае, если она находится в другом файле, вы должны изменить путь к исправлению, например: @mock.patch('your_package.your_file.your_function.random.choice')

* (2) В этом случае get_random_words функция вызывается random.choice 3 раза. Вот почему вы должны поместить в него равные или более элементов mock_choice.side_effect . Это потому, что если в нем меньше элементов, он выдаст StopIteration ошибку.

Комментарии:

1. 1 за добавление опции для заглушки функции с разными значениями в цикле. Это достижимо с помощью pytest-mock?