我的設計使我陷入了我認為是一個復雜的 pytest 問題。我確信我不知道采取正確的方法。我正在模擬一個復雜的紙牌游戲,稱為“利物浦拉米牌”。用戶界面使用 Kivy,但我在測驗中遇到的問題可能會出現在任何 GUI 框架中,無論是 tkinter、QT、wxPython 還是其他任何東西。
在拉米模擬器中,我想在不實際啟動 kivy 的情況下測驗邏輯。我認為這意味著我需要self在許多方法中模擬,因為這些方法通過“self.method_name”相互呼叫。我對嘲笑的許多帖子的閱讀self或模擬全域或模塊級變數讓我很困惑。我不想開始 kivy 至少有兩個原因。首先,經過大量初始化后,它會進入“play_game”方法。雖然我可以從代碼中呼叫它,而不是按下按鈕,但它會立即得到一副洗牌的紙牌和一堆棄牌,并向玩家(所有人都是機器人)隨機發一手牌,然后每個人都拿一個回合。但我需要做的測驗是設定這三個變數(牌組、棄牌、手牌)并運行大約 50 種變化。其次,這似乎違背了盡可能多地隔離單元測驗的目標。
因此,我沒有實體化類和測驗方法,而是直接從類中呼叫方法。這是一個非常簡化的示例:
class Turn(Widget):
"""The Turn class contains the actions a player can perform, such as draw, pick-up, meld, and knock.
Although this is an abstract class, it must inherit from Widget for the event processing to work."""
def __init__(self, round):
super().__init__()
# omitting a bunch of attributes
self.goal_met = False
self.remaining_cards = []
def evaluate_hand(self, goal, hand):
"""Compares hand to round goal and determines desired list of cards."""
# lots of complicated stuff here
self.check_goal_met(hand, sets_needed, sets, runs_needed, runs)
# more logic
return cards_needed
def check_goal_met(self, hand, sets_needed, sets, runs_needed, runs):
"""Determine if the round goal has been met, and what are the remaining cards"""
try:
# lots of logic omitted
remaining_cards = list(set(hand).difference(set(temp_hand)))
if goal_met and remaining_cards == []:
self.can_go_out = True
return (goal_met, remaining_cards)
@pytest.mark.parametrize('goal, case, output', sr_test_cases)
def test_evaluate_hand(goal, case, output):
round_goals = list(GOALS.keys())
hand = build_test_hand(case)
needs = Turn.evaluate_hand(None, goal, hand) # using None in place of 'self'
assert needs==output
When you call a method directly from the class, the first argument must be the class identifier, typically 'self'
If I call it like this Turn.evaluate_hand(goal, hand), then I'll get TypeError: evaluate_hand() missing 1 required positional argument: 'hand' Here goal is considered self, then hand is considered goal and there is no hand which generates the error message.
When called as shown Turn.evaluate_hand(None, goal, hand) then the test when run will get to:
self.check_goal_met(hand, sets_needed, sets, runs_needed, runs), and will then generate for every test case:
AttributeError: 'NoneType' object has no attribute 'check_goal_met'. It needs to see the namespace for the Turn class, including the methods, check_goal_met. But None isn't a namespace and so when it effectively does None.check_goal_met it generates the AttributError.
You can hack this by doing:
def evaluate_hand(self, goal, hand):
"""Compares hand to round goal and determines desired list of cards."""
# lots of complicated stuff here
# self.check_goal_met(hand, sets_needed, sets, runs_needed, runs)
if self is not None:
goal_met, remaining_cards = self.check_goal_met(hand, sets_needed, sets, runs_needed, runs)
else:
goal_met, remaining_cards = Turn.check_goal_met(self, hand, sets_needed, sets, runs_needed, runs)
# more logic
But all that does is (a) pollute your code, and (b) defer the problem to the next call. For the None argument gets passed to check_goal_met, and that will promptly generate an AttributeError on the line self.can_go_out = True. Of course you can in turn hack THAT by adding can_go_out to the return tuple.
By now I concluded this whole approach was wrong because it was implying that I'd all but have to abandon using attributes in the functions.
To get it to work, I need to instantiate Turn, not just call the class. But that seems unreasonably hard. Here is the object structure. "App" and "config" are kivy objects.
RummySimulator(App) →
BaseGame(object) →
Sets_and_Runs(BaseGame) →
Self.game.play_game →
Round(app, game) →
Turn(round)
Player(num, config) → ( Hand(), Melds(hand) )
I tried for a day to setup all this in the test case, and finally gave up. The whole point of a test case is to isolate the test function from the rest of the application, not reproduce the complication of the rest of the application in the test case.
It seems to me that if I mock `self1 in the call to Turn in the test case, that won't work. It's a mock, not a namespace, so the calls to the other methods will fail.
So can I mock the class Turn, in such a way that it lets the code find the methods and use the attributes? I'm really stuck, and think my whole approach must be wrong.
What is the right approach? And is there some doc I can study on how to do it? thanks in advance.
uj5u.com熱心網友回復:
對我來說,聽起來您應該重構代碼以將游戲邏輯與 GUI 邏輯分離。
我認為在測驗中需要使用模擬通常表明代碼可以設計得更好。 這是一篇文章,我可以更清楚地解釋這個想法。一個特別相關的引述:
為了單元測驗的目的而需要mock以實作單元隔離是由單元之間的耦合引起的。緊耦合使代碼更加僵硬和脆弱:在需要更改時更容易中斷。一般來說,減少耦合本身是可取的,因為它使代碼更易于擴展和維護。通過消除對模擬的需求,它還使測驗更容易,這一事實只是錦上添花。
由此我們可以推斷,如果我們在模擬某些東西,可能有機會通過減少單元之間的耦合來使我們的代碼更加靈活。一旦完成,您將不再需要模擬。
在您的情況下,緊密耦合是在 GUI 和游戲邏輯之間。我建議將所有游戲邏輯移動到與 GUI 無關的函式/類中。理想情況下,盡可能多的邏輯最終會出現在純函式中。這將使撰寫測驗和擴展/維護代碼變得更加容易。
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/450116.html
標籤:unit-testing mocking kivy pytest pytest-mock
上一篇:笑話toHaveReturnedWithundefined
下一篇:將C#轉換為VB.NET'ServerCertificateValidationCallback'不是'HttpWebRequest'的事件
