How to partially mock an object inside legacy code with OCMock?
Asked Answered
C

3

1

I would like to accomplish what also is described here, i.e create mocks inside legacy code. However I require partial instead of nice or strict mocks.

For example, consider leaderboards that behave exactly like GKLeaderbaord except for implementing a stubbed version of loadScoresWithCompletionHandler:.

I've tried this code inside an XCTestCase but it currently fails at runtime in the indicated line: OCMInvocationMatcher raises an EXC_BAD_ACCESS error. Perhaps there is some infinite recursion going on.

id leaderboardMock = OCMClassMock(GKLeaderboard.class);
OCMStub([leaderboardMock alloc])
    .andReturn(OCMPartialMock([GKLeaderboard alloc]));
OCMStub([leaderboardMock loadScoresWithCompletionHandler: [OCMArg any]])
    .andDo(^(NSInvocation *invocation) { /* ... */ });

// these parts normally nested inside legacy code

GKLeaderboard *leaderboard = /* raises EXC_BAD_ACCESS */
    [[GKLeaderboard alloc] initWithPlayers: @[ GKLocalPlayer.localPlayer ]];
leaderboard.identifier = @"Test";

[leaderboard loadScoresWithCompletionHandler: nil /* ... */ ];

What am I doing wrong and is this even possible for partial mockups?

UPDATE I can by now see how the indicated line might (quite obviously) cause an infinite recursion, but don't yet know how to avoid (or break) it.

UPDATE I've also had no success with an attempt of bringing in an dedicated class with OCMStub([leaderboardMock alloc]).andReturn([LeaderboardMock alloc]) (nor with OCMStub([leaderboardMock initWithPlayers: [OCMArg any]]).andReturn([[LeaderboardMock alloc] initWithPlayers:nil])). Perhaps OCMock does its magic at the level of init (the documentation says: "it is not possible to stub the init method, because that is implemented by the mock itself") hence such an attempt the level of alloc (or initWithPlayers:) cannot have its desired effect.

Cyclist answered 30/1, 2015 at 16:59 Comment(0)
C
0

I have by now concluded that method swizzling would be a possible choice.

A replacement method could e.g. generate a partial mockup from within the context of legacy code and hence introduce a partial mock in that context without requiring changes to legacy APIs.

Cyclist answered 14/2, 2015 at 15:21 Comment(0)
M
1

Not sure I follow what you are trying to do. It seems like a misunderstanding. Does the following not work for you?

GKLeaderboard *leaderboard = [[GKLeaderboard alloc] initWithPlayers: ... ];
id leaderboardMock = OCMPartialMock(leaderboard);
OCMStub([leaderboarMock loadScoresWithCompletionHandler: ...]);

You can use the normal object without restrictions. You can use the partial mock created for the object to manipulate the actual instance in leaderboard. That's the beauty of partial mocks.

UPDATE: If the object creation is not under your control, you can try the following:

GKLeaderboard *leaderboard = [[GKLeaderboard alloc] initWithPlayers: ... ];
id leaderboardMock = OCMPartialMock(leaderboard);

OCMStub([leaderboardMock alloc]).andReturn(leaderboardMock);
OCMStub([leaderboardMock initWithPlayers:[OCMArg any]).andReturn(leaderboard);

OCMStub([leaderboarMock loadScoresWithCompletionHandler: ...]);
Malta answered 2/2, 2015 at 10:16 Comment(8)
The problem (as I perceive it now) is that the leaderboard is created deep down in legacy code, which I cannot touch. Hence I have no means of placing the call id leaderboardMock = OCMPartialMock(leaderboard); between its creation and use (between your 1st and 3rd lines of code so to speak). See also here.Cyclist
You can't mock the init method but initWithPlayers: should work. So maybe try mocking alloc/init.Malta
For calling OCMPartialMock (as opposed to OCMClassMock) you need to know the instance of leaderboard. Unfortuntately, it is only known after its creation inside the legacy code base and not exposed by some API ...Cyclist
BTW, mocking initWithPlayers: did not work for me either (as mentioned in Q.)Cyclist
Have you tried to do this exactly like I suggest? I see in the original question that you are creating an object inside the stub setup, which means that you don't have a reference to it and therefore can't create a partial mock on it. Also, mixing a class mock and a partial mock and then stubbing class methods is bound to lead to problems, too. Hence, the specific solution I suggested in the updated answer.Malta
But that's exactly the issue: outside the legacy code I don't have a reference, this is why the challenge occurs in the first place. One can deal with this in the case of weak/strict mock (see linked A), but evidently/so far not partial mocks.Cyclist
In the code I posted in the update I create the instance, and set up the mock on that instance. This needs to happen early. I also stub the alloc/init... methods so that the legacy code, when it calls alloc/init will get the instance created in the test.Malta
Thx for your effort, but this approach is not appropriate. (I've tried it before). What players (...) would you pass in "early" to match what the legacy code chooses "late"? IMHO there really would have to be a way for configuring mocked allocs or inits such that they return new mocked instances (not just a singleton or pre-allocated instances perhaps from a pool).Cyclist
C
0

I have by now concluded that method swizzling would be a possible choice.

A replacement method could e.g. generate a partial mockup from within the context of legacy code and hence introduce a partial mock in that context without requiring changes to legacy APIs.

Cyclist answered 14/2, 2015 at 15:21 Comment(0)
A
0

you should not use following line, it will mock your entire class and none of real object will get called.

OCMClassMock(GKLeaderboard.class)

Abulia answered 14/7, 2015 at 11:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.