2014-04-24 13 views
7

Come faccio a prendere in giro una classe con metodi non associati? Ad esempio, questa classe ha un @classmethod e un @staticmethod:Come prendere in giro metodi statici e metodi di classe 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 

È possibile che questo descrive più o meno la mia domanda. Quello che segue è un esempio funzionante che dimostra le cose che ho provato.

Classe Machine contiene un'istanza di Calculator. Sto testando Machine con una simulazione di Calculator. Per dimostrare la mia edizione, Machine chiama i metodi non legate tramite un'istanza di Calculator e tramite la classe 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 

Tutto il codice funzionale sopra funziona bene. La prossima è la parte che non funziona.

Creo una finta di Calculator da utilizzare nei test 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 

Nel test dell'unità sottostante, i metodi vincolati usano MockCalculator come avevo sperato. Tuttavia, le chiamate a Calculator.increment() e Calculator.decrement() usano ancora 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 

così cerco di rattoppare Calculator.increment() e 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) 

Anche dopo l'applicazione di patch, i metodi non legate vogliono un'istanza di Calculator come argomento :

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

Come faccio mock di metodo di classe 012.347.956,906 milae metodo statico Calculator.decrement()?

risposta

2

La soluzione è utilizzare invece le funzioni del modulo. Le funzioni del modulo sono più Pythonic, comunque. Il mio uso eccessivo di metodi di classe e statici è stato influenzato dall'esperienza passata con C#.

Quindi, per prima cosa, ecco il software refactored in prova, con i metodi increment() e decrement() come funzioni del modulo. L'interfaccia non cambia, ma la funzionalità è la stessa:

# 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 

aggiungere funzioni increment_mock() e decrement_mock() per deridere increment() e 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 

E ora per la parte buona. Patch increment() e decrement() di sostituirli con i loro schernisce:

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

questa è la risposta corretta. [su un'altra domanda] viene discusso il metodo statico rispetto al metodo dei moduli] (http://programmers.stackexchange.com/questions/112137/is-staticmethod-proliferation-a-code-smell) e la conclusione è che i metodi statici sono un odore di codice e un'imitazione di stile Java in cui le definizioni delle funzioni dei moduli non esistono ei metodi statici sono l'unico sostituto. –

+14

Questo non risponde alla domanda. 'staticmethod' è un costrutto Python valido e sapere come deridere queste funzioni è prezioso. "Fai qualcos'altro" non è la risposta giusta, soprattutto considerando che puoi prendere in giro i metodi statici. – AnilRedshift

5

Si stava riparando l'oggetto sbagliato. È necessario applicare una patch alla classe Calculator dalla classe Machine, non alla classe generale Calculator. Leggi su di esso 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

Grazie per la vostra risposta.Sto testando la classe Machine, quindi non è soddisfacente applicare metodi come Machine.mult(). Inoltre, il finto MockComputer.multiplier() funziona bene. La mia domanda riguardava la simulazione o l'applicazione di patch ai metodi statici e di classe Computer.increment() e Computer.decrement(). –