I'm trying to write XCTest and inject mocked dependency with Typhoon.
Here is code in my ViewController:
- (instancetype)init {
self = [super init];
MDMainAssembly *assembly = (MDMainAssembly *) [TyphoonComponentFactory defaultFactory];
self.alertManager = [assembly alertManager];
return self;
}
Here is how I'm trying to change injection:
self.mockedAlertManager = mock([MDAlertManager class]);
MDMainAssembly *assembly = [MDMainAssembly assembly];
TyphoonComponentFactory *factory = [TyphoonBlockComponentFactory factoryWithAssembly:assembly];
TyphoonPatcher *patcher = [[TyphoonPatcher alloc] init];
[patcher patchDefinition:[assembly alertManager] withObject:^id {
return self.mockedAlertManager;
}];
[factory attachPostProcessor:patcher];
However tests are failing because this factory not possible to set as default. I configure in AppDelegate factory:
TyphoonComponentFactory *factory = [[TyphoonBlockComponentFactory alloc] initWithAssemblies:#[
[MDMainAssembly assembly],
]];
[factory makeDefault];
How to get out of this situation?
We created the defaultFactory feature for a limited number of cases. The main is:
Getting access to Typhoon and looking up a dependency for a class that isn't being managed by Typhoon. Generally this isn't required.
Although you could use it in tests, we recommend instead creating and destroying a Typhoon container for each test run. To avoid duplication, you could create a method as follows:
#implementation IntegrationTestUtils
+ (TyphoonComponentFactory*)testAssembly
{
TyphoonComponentFactory* factory = [[TyphoonBlockComponentFactory alloc] initWithAssemblies:#[
[MyAppAssembly assembly],
[MyAppKernel assembly],
[MyAppNetworkComponents assembly],
[MyAppPersistenceComponents assembly]
]];
id <TyphoonResource> configurationProperties = [TyphoonBundleResource withName:#"Configuration.properties"];
[factory attachPostProcessor:[TyphoonPropertyPlaceholderConfigurer configurerWithResource:configurationProperties]];
return factory;
}
. . if required you could attach a patcher to this assembly.
Attaching a patcher to the default factory:
If you were to apply a patcher to the default assembly, you'd most probably want to un-patch again. This feature is in the backlog here.
Related
My code invokes a C library function:
#implementation Store
...
-(void) doWork {
// this is a C function from a library
int data = getData();
...
}
end
I am unit testing the above function, I want to mock the C function getData() in my test, here is my test case:
#interface StoreTests : XCTestCase {
int mData;
Store *store;
}
#end
#implementation StoreTests
-(void) setUp {
[super setUp];
mData = 0;
store = [[Store alloc] init];
}
-(void) testDoWork {
// this call will use the mocked getData(), no problem here.
[store doWork];
}
// mocked getData()
int getData() {
mData = 10; // Use of undeclared identifier 'mData', why?
return mData;
}
...
#end
Why I get complier error:
Use of undeclared identifier 'mData' inside mocked getData() function?
You are misunderstanding how instance methods and variables work.
Every instance method has a variable self which references the current instance (or "current object") and a use of an instance variable, such as mData, is shorthand for accessing that variable using self, e.g self->mData, where -> is the (Objective-)C operator for field access. So your setup method written "long hand" is:
-(void) setUp {
[super setUp];
self->mData = 0;
self->store = [[Store alloc] init];
}
But where does self, the reference to the instance, itself come from? Well it's not magical, just hidden, it is passed to an instance method automatically as a hidden extra argument. At this point which switch to pseudo-code to show this. Your setup method is effectively compiled as:
-(void) setUp withSelf:(StoreTest *)self {
[super setUp];
self->mData = 0;
self->store = [[Store alloc] init];
}
and a call such as:
StoreTests *myStoreTests = ...
[myStoreTests setup];
is effectively compiled as something like:
[myStoreTests setup withSelf:myStoreTests];
automatically adding the extra self argument.
Now all the above only applies to methods, and enables them to access instance variables and methods, it does not apply to plain C functions - they have no hidden self argument and cannot access instance variables.
The solution you mention in the answer you added of declaring mData outside of the interface:
int mData;
#interface StoreTests : XCTestCase {
Store *store;
}
#end
changes mData into a global variable, instead of being an instance variable. C functions can access global variables. However this does mean that every instance of the class shares the same mData, there is only one mData in this case rather than one for every instance.
Making an instance variable into a global is therefore not a general solution to to issues like this, however as it is unlikely that you will have more than one instance of your StoreTests class it is a suitable solution in this case.
You should however make one change: you can only have one global variable with a given name with a program, so your mData must be unique and is accessible by any code within your program, not just the code of StoreTests. You can mitigate this my declaring the variable as static:
static int mData;
this keeps the variable as global but only makes it visible to code within the same file as the declaration, which is probably just the code of StoreTests.
HTH
I found one solution for my question, that is declare mData above #interface StoreTests : XCTestCase, something like this:
int mData;
#interface StoreTests : XCTestCase {
Store *store;
}
#end
...
I have a simple School class which defines a init method:
#implementation School
- (id) init {
self = [super init];
if (self) {
// call class method of MyHelper class
if ([MyHelper isWeekend]) {
[MyHelper doSomething];
}
}
}
#end
(MyHelper is a class contains only class methods, the isWeekend is a class method returns a boolean value)
I use OCMock to unit test this simple init method:
- (void)testInit {
// mock a school instance
id schoolMock = [OCMockObject partialMockForObject:[[School alloc] init]];
// mock class MyHelper
id MyHelperMock = OCMStrictClassMock([MyHelper class]);
// stub class method 'isWeekend()' to return true
OCMExpect([MyHelperMock isWeekend]).andReturn(true);
// run init method
[schoolMock init];
// verify
OCMVerify([MyHelperMock isWeekend]);
}
But when run it, I get error:
OCMockObject(MyHelper): Method isWeekend was not invoked. why?
You've created a mock for the MyHelper class, but this isn't going to be used within the implementation of your School object. You'd only get the mocked response if you wrote [MyHelperMock isWeekend], which you can't do inside the initialiser without rewriting it for tests.
To make your School class more testable you should be passing in any dependencies on initialisation. For example, you could pass in the isWeekend value as part of the initialiser, instead of obtaining it inside the method, or pass in the class object (MyHelper or MyHelperMock).
It's worth noting that finding certain classes or methods difficult to test because of things like this is often a good indicator that your code isn't structured very well.
This question already has an answer here:
How to mock an object with OCMock which isn't passed as a parameter to method?
(1 answer)
Closed 6 years ago.
I am using OCMock 3 to write my unite tests in iOS project.
I have a foo method under School class:
#implementation School
-(NSString *)foo:
{
// I need to mock this MyService instance in my test
MyService *service = [[MyService alloc] init];
// I need to stub the function return for [service getStudent]
Student *student = [service getStudent];
if (student.age == 12) {
//log the age is 12
} else {
//log the age is not 12
}
...
}
The Student looks like this:
#interface Student : NSObject
#property NSInteger age;
...
#end
In my test case, I want to stub the method call [service getStudent] to return a Student instance with age value 12 I defined:
// use mocked service
id mockService = OCMClassMock([MyService class]);
OCMStub([[mockService alloc] init]).andReturn(mockService);
// create a student instance (with age=12) which I want to return by function '-(Student*) getStudent:'
Student *myStudent = [[Student alloc] init];
myStudent.age = 12;
// stub function to return 'myStudent'
OCMStub([mockService getStudent]).andReturn(myStudent);
// execute foo method
id schoolToTest = [OCMockObject partialMockForObject:[[School alloc] init]];
[schoolToTest foo];
When I run my test case, however, the student returned by -(Student*)getStudent: method is not with age 12, why?
===== UPDATE ====
I noticed in internet, somebody suggested to separate the alloc and init to stub. I also tried it but it doesn't work as it says:
// use mocked service
id mockService = OCMClassMock([MyService class]);
OCMStub([mockService alloc]).andReturn(mockService);
OCMStub([mockService init]).andReturn(mockService);
// stub function to return 'myStudent'
OCMStub([mockService getStudent]).andReturn(myStudent);
When I do this & run my test case, the real implementation of -(Student*)getStudent: method get called... I can't understand why people says it works.
You cannot mock the init method. This is stated in the documentation (Section 9.3), but maybe it's too hidden.
I have Assembly:
#interface MDUIAssembly : TyphoonAssembly
#property (nonatomic, strong, readonly) MDServiceAssembly *services;
#property (nonatomic, strong, readonly) MDModelAssembly *models;
- (id)choiceController;
#end
#implementation MDUIAssembly
- (void)resolveCollaboratingAssemblies
{
_services = [TyphoonCollaboratingAssemblyProxy proxy];
_models = [TyphoonCollaboratingAssemblyProxy proxy];
}
- (id)choiceController
{
return [TyphoonDefinition withClass:[MDChoiceViewController class]
configuration: ^(TyphoonDefinition *definition) {
[definition useInitializer:#selector(initWithAnalytics:diary:)
parameters: ^(TyphoonMethod *initializer) {
[initializer injectParameterWith:[_services analytics]];
[initializer injectParameterWith:[_models diary]];
}];
}];
}
#end
Here what I'm trying to do in tests:
- (void)setUp
{
patcher = [TyphoonPatcher new];
MDUIAssembly *ui = (id) [TyphoonComponentFactory defaultFactory];
[patcher patchDefinition:[ui choiceController] withObject:^id{
return mock([MDChoiceViewController class]);
}];
[[TyphoonComponentFactory defaultFactory] attachPostProcessor:patcher];
}
- (void) tearDown
{
[super tearDown];
[patcher rollback];
}
Unfortunately my setUp fails with next message:
-[MDChoiceViewController key]: unrecognized selector sent to instance 0xbb8aaf0
What I'm doing wrong?
You've encountered a poor design choice on Typhoon's part, but there's an easy work-around.
You're using this method:
[patcher patchDefinition:[ui choiceController] withObject:^id{
return mock([MDChoiceViewController class]);
}];
. . which is expecting a TyphoonDefinition as argument. When bootstrapping Typhoon:
We start with one ore more TyphoonAssembly subclasses, which Typhoon instruments to obtain recipes for building components. These TyphoonAssembly sub-clases are then discarded.
We now have a TyphoonComponentFactory that will allow any of your TyphoonAssembly interfaces to pose in front of it. (This is so you can have multiple configs of the same class, while still avoiding magic strings, allows auto-completion in your IDE, etc).
When the TyphoonPatcher was written it was designed for the case where you obtain a new TyphoonComponentFactory for your tests (recommended), like this:
//This is an actual TyphoonAssembly not the factory posing as an assembly
MiddleAgesAssembly* assembly = [MiddleAgesAssembly assembly];
TyphoonComponentFactory* factory = [TyphoonBlockComponentFactory factoryWithAssembly:assembly];
TyphoonPatcher* patcher = [[TyphoonPatcher alloc] init];
[patcher patchDefinition:[assembly knight] withObject:^id
{
Knight* mockKnight = mock([Knight class]);
[given([mockKnight favoriteDamsels]) willReturn:#[
#"Mary",
#"Janezzz"
]];
return mockKnight;
}];
[factory attachPostProcessor:patcher];
Knight* knight = [(MiddleAgesAssembly*) factory knight];
What happened:
So the problem is that the TyphoonPatcher is expecting TyphoonDefinition from the TyphoonAssembly and instead it is getting an actual component from a TyphoonComponentFactory.
Very confusing, and that way of obtaining a patcher should be deprecated.
Solution:
Use the following instead:
[patcher patchDefinitionWithSelector:#selector(myController) withObject:^id{
return myFakeController;
}];
Here's some extra advice to go along with the main answer . . .
Unit Tests vs Integration Tests:
In Typhoon we adhere to the traditional terms:
Unit Tests : Testing your class in isolation from collaborators. This is where you inject test doubles like mocks or stubs in place of all of the real dependencies.
Integration Tests: Testing your class using real collaborators. Although you may patch our a component in order to put the system in the required state for that test.
So any test that uses TyphoonPatcher would probably be an integration test.
More info here: Typhoon Integration Testing
Resolve Collaborating Assemblies:
This was required in earlier version of Typhoon, but is not longer needed. Any properties that are are sub-class of TyphoonAssembly will be treated as collaborating assemblies. Remove the following:
- (void)resolveCollaboratingAssemblies
{
_services = [TyphoonCollaboratingAssemblyProxy proxy];
_models = [TyphoonCollaboratingAssemblyProxy proxy];
}
Tests instantiate their own assembly:
We recommend that tests instantiate and tear down their on TyphoonComponentFactory. The advantages are:
[TyphoonComponentFactory defaultFactory] is a global and has some drawbacks.
Integration tests can define their own patches without having to worry about putting the system back in the original state.
In addition to using TyphoonPatcher, if you wish you can create an assembly where the definitions for some components are overridden.
I'm currently trying to learn how to use private frameworks, just to gain some deeper understanding of these kinds of things.
So, while experimenting with ToneLibrary.framework (See classdump), I noticed that I am able to work with existing instances, but am unable to instantiate a class object from a private framework like I normally would myself.
For example, from the framework mentioned above, I have imported and TLToneManager.h and TLITunesTone.h.
I can do:
NSArray *tones = [[TLToneManager sharedRingtoneManager] installedTones];
for (id object in tones) {
TLITunesTone *tone = (TLITunesTone *)object;
NSLog(#"Tone: %#", [tone name]);
NSLog(#"Tone: %#", [tone filePath]);
}
But I can't do:
TLITunesTone *newTone = [[TLITunesTone alloc] init];
[newTone setName:#"TestTone"];
as it will result in "the symbol for TLITunesTone cannot be found for architecture arm7v".
If I add Class TLITunesTone = NSClassFromString(#"TLITunesTone"); before that code, it will complain that newTone is an undeclared identifier. I tried forward declaring the class, with the same result.
This however will work:
Class TLITunesTone = NSClassFromString(#"TLITunesTone");
id newTone = [[TLITunesTone alloc] init];
[newTone setName:#"TestTone"];
And I can now continue use newTone normally.
So my question is: Why can't I use a static type for an instance of a class which is part of a private framework?