2014-04-24 15 views
7

Jak kpić z klasy, która ma niezwiązane metody? Na przykład, ta klasa ma @classmethod i @staticmethod:Jak wyśmiewać metody statyczne i metody klasowe w języku Python

class Calculator(object): 
    def __init__(self, multiplier): 
     self._multiplier = multiplier 
    def multiply(self, n): 
     return self._multiplier * n 
    @classmethod 
    def increment(cls, n): 
     return n + 1 
    @staticmethod 
    def decrement(n): 
     return n - 1 

calculator = Calculator(2) 
assert calculator.multiply(3) == 6  
assert calculator.increment(3) == 4 
assert calculator.decrement(3) == 2 
assert Calculator.increment(3) == 4 
assert Calculator.decrement(3) == 2 

Powyższy dość dużo opisuje moje pytanie. Poniżej znajduje się działający przykład demonstrujący rzeczy, które próbowałem.

Klasa Machine zawiera wystąpienie Calculator. Będę testować Machine z próbą Calculator. Do wykazania mój problem, Machine wywołuje metody niezwiązanych poprzez wystąpienie Calculator i za pośrednictwem klasy Calculator:

class Machine(object): 
    def __init__(self, calculator): 
     self._calculator = calculator 
    def mult(self, n): 
     return self._calculator.multiply(n) 
    def incr_bound(self, n): 
     return self._calculator.increment(n) 
    def decr_bound(self, n): 
     return self._calculator.decrement(n) 
    def incr_unbound(self, n): 
     return Calculator.increment(n) 
    def decr_unbound(self, n): 
     return Calculator.decrement(n) 

machine = Machine(Calculator(3)) 
assert machine.mult(3) == 9 

assert machine.incr_bound(3) == 4 
assert machine.incr_unbound(3) == 4 

assert machine.decr_bound(3) == 2 
assert machine.decr_unbound(3) == 2 

przede wszystkim funkcjonalny kod działa prawidłowo. Dalej jest ta część, która nie działa.

utworzyć próbną Calculator do wykorzystania w testach Machine:

from mock import Mock 

def MockCalculator(multiplier): 
    mock = Mock(spec=Calculator, name='MockCalculator') 

    def multiply_proxy(n): 
     '''Multiply by 2*multiplier instead so we can see the difference''' 
     return 2 * multiplier * n 
    mock.multiply = multiply_proxy 

    def increment_proxy(n): 
     '''Increment by 2 instead of 1 so we can see the difference''' 
     return n + 2 
    mock.increment = increment_proxy 

    def decrement_proxy(n): 
     '''Decrement by 2 instead of 1 so we can see the difference''' 
     return n - 2 
    mock.decrement = decrement_proxy 

    return mock 

W teście jednostkowej poniżej metody bound użyciu MockCalculator, jak się spodziewano. Jednak rozmowy do Calculator.increment() i Calculator.decrement() nadal korzystać Calculator:

import unittest 

class TestMachine(unittest.TestCase): 
    def test_bound(self): 
     '''The bound methods of Calculator are replaced with MockCalculator''' 
     machine = Machine(MockCalculator(3)) 
     self.assertEqual(machine.mult(3), 18) 
     self.assertEqual(machine.incr_bound(3), 5) 
     self.assertEqual(machine.decr_bound(3), 1) 

    def test_unbound(self): 
     '''Machine.incr_unbound() and Machine.decr_unbound() are still using 
     Calculator.increment() and Calculator.decrement(n), which is wrong. 
     ''' 
     machine = Machine(MockCalculator(3)) 
     self.assertEqual(machine.incr_unbound(3), 4) # I wish this was 5 
     self.assertEqual(machine.decr_unbound(3), 2) # I wish this was 1 

Więc staram się załatać Calculator.increment() i Calculator.decrement():

def MockCalculatorImproved(multiplier): 
    mock = Mock(spec=Calculator, name='MockCalculatorImproved') 

    def multiply_proxy(n): 
     '''Multiply by 2*multiplier instead of multiplier so we can see the difference''' 
     return 2 * multiplier * n 
    mock.multiply = multiply_proxy 
    return mock 

def increment_proxy(n): 
    '''Increment by 2 instead of 1 so we can see the difference''' 
    return n + 2 

def decrement_proxy(n): 
    '''Decrement by 2 instead of 1 so we can see the difference''' 
    return n - 2 


from mock import patch 

@patch.object(Calculator, 'increment', increment_proxy) 
@patch.object(Calculator, 'decrement', decrement_proxy) 
class TestMachineImproved(unittest.TestCase): 
    def test_bound(self): 
     '''The bound methods of Calculator are replaced with MockCalculator''' 
     machine = Machine(MockCalculatorImproved(3)) 
     self.assertEqual(machine.mult(3), 18) 
     self.assertEqual(machine.incr_bound(3), 5) 
     self.assertEqual(machine.decr_bound(3), 1) 

    def test_unbound(self): 
     '''machine.incr_unbound() and Machine.decr_unbound() should use 
     increment_proxy() and decrement_proxy(n). 
     ''' 
     machine = Machine(MockCalculatorImproved(3)) 
     self.assertEqual(machine.incr_unbound(3), 5) 
     self.assertEqual(machine.decr_unbound(3), 1) 

Nawet po łatanie, metody niezwiązane chcą wystąpienie Calculator jako argument :

TypeError: unbound method increment_proxy() must be called with Calculator instance as first argument (got int instance instead)

Jak wyśmiewać metodę klasy Calculator.increment() i metoda statyczna Calculator.decrement()?

Odpowiedz

2

Rozwiązaniem jest użycie funkcji modułu. Funkcje modułu są jednak bardziej Pythoniczne. Na moje nadużywanie klas i metod statycznych miało wpływ wcześniejsze doświadczenie z C#.

Najpierw testowane oprogramowanie refaktoryzowane, z wykorzystaniem metod increment() i decrement() jako funkcji modułu. Interfejs nie zmienia, ale funkcjonalność jest taka sama:

# Module machines 

class Calculator(object): 
    def __init__(self, multiplier): 
     self._multiplier = multiplier 
    def multiply(self, n): 
     return self._multiplier * n 

def increment(n): 
    return n + 1 

def decrement(n): 
    return n - 1 

calculator = Calculator(2) 
assert calculator.multiply(3) == 6 
assert increment(3) == 4 
assert decrement(3) == 2 


class Machine(object): 
    '''A larger machine that has a calculator.''' 
    def __init__(self, calculator): 
     self._calculator = calculator 
    def mult(self, n): 
     return self._calculator.multiply(n) 
    def incr(self, n): 
     return increment(n) 
    def decr(self, n): 
     return decrement(n) 

machine = Machine(Calculator(3)) 
assert machine.mult(3) == 9 
assert machine.incr(3) == 4 
assert machine.decr(3) == 2 

Dodaj funkcje increment_mock() i decrement_mock() drwić increment() i decrement():

from mock import Mock 
import machines 

def MockCalculator(multiplier): 
    mock = Mock(spec=machines.Calculator, name='MockCalculator') 

    def multiply_proxy(n): 
     '''Multiply by 2*multiplier instead of multiplier so we can see the 
     difference. 
     ''' 
     return 2 * multiplier * n 
    mock.multiply = multiply_proxy 

    return mock 

def increment_mock(n): 
    '''Increment by 2 instead of 1 so we can see the difference.''' 
    return n + 2 

def decrement_mock(n): 
    '''Decrement by 2 instead of 1 so we can see the difference.''' 
    return n - 2 

A teraz dobrej strony. Łatka increment() i decrement(), aby zastąpić je swoimi próbkami:

import unittest 
from mock import patch 
import machines 

@patch('machines.increment', increment_mock) 
@patch('machines.decrement', decrement_mock) 
class TestMachine(unittest.TestCase): 
    def test_mult(self): 
     '''The bound method of Calculator is replaced with MockCalculator''' 
     machine = machines.Machine(MockCalculator(3)) 
     self.assertEqual(machine.mult(3), 18) 

    def test_incr(self): 
     '''increment() is replaced with increment_mock()''' 
     machine = machines.Machine(MockCalculator(3)) 
     self.assertEqual(machine.incr(3), 5) 

    def test_decr(self): 
     '''decrement() is replaced with decrement_mock()''' 
     machine = machines.Machine(MockCalculator(3)) 
     self.assertEqual(machine.decr(3), 1) 
+2

to jest poprawna odpowiedź. [w innym pytaniu] problem metody statycznej vs metody modułu] (http://programmers.stackexchange.com/questions/112137/is-staticmethod-proliferation-a-code-smell) jest omawiany, a wniosek jest taki, że metody statyczne są zapach kodu i imitacja stylu Java, w którym nie istnieją definicje funkcji modułów, a statyczne metody są jedynymi substytutami. –

+14

To nie odpowiada na pytanie. 'staticmethod' jest poprawną konstrukcją Pythona i wiedza o tym, jak kpić z tych funkcji jest cenna. "Zrób coś innego" nie jest właściwą odpowiedzią, szczególnie biorąc pod uwagę, że możesz kpić z metod statycznych. – AnilRedshift

5

Naprawiałeś niewłaściwy obiekt. Należy załączyć Calculator z klasy Machine, a nie ogólną klasę Calculator. Przeczytaj o tym here.

from mock import patch 
import unittest 

from calculator import Calculator 
from machine import Machine 


class TestMachine(unittest.TestCase): 
    def my_mocked_mult(self, multiplier): 
     return 2 * multiplier * 3 
    def test_bound(self): 
     '''The bound methods of Calculator are replaced with MockCalculator''' 
     machine = Machine(Calculator(3)) 
     with patch.object(machine, "mult") as mocked_mult: 
      mocked_mult.side_effect = self.my_mocked_mult 
      self.assertEqual(machine.mult(3), 18) 
      self.assertEqual(machine.incr_bound(3), 5) 
      self.assertEqual(machine.decr_bound(3), 1) 

    def test_unbound(self): 
     '''Machine.incr_unbound() and Machine.decr_unbound() are still using 
     Calculator.increment() and Calculator.decrement(n), which is wrong. 
     ''' 
     machine = Machine(Calculator(3)) 
     self.assertEqual(machine.incr_unbound(3), 4) # I wish this was 5 
     self.assertEqual(machine.decr_unbound(3), 2) # I wish this was 1 
+0

Dziękuję za odpowiedź.Testuję maszynę klasy, więc nie jest zadowalająca łatać metod takich jak Machine.mult(). Poza tym fałszywy MockComputer.multiplier() działa dobrze. Moje pytanie dotyczyło szyderstwa lub łatania metod statycznych i klasowych Computer.increment() i Computer.decrement(). –