TestsTested | ✓ |
LangLanguage | Obj-CObjective C |
License | MIT |
ReleasedLast Release | Oct 2015 |
Maintained by Michael England.
A simple library to help define model factories for use when testing your iOS/Mac applications. Heavily based on FactoryGirl for Ruby.
We have a test on class SomeClass
that requires an instance of Data Value Object User
. The class only makes use of the username
property.
In the test, we have to generate a valid User object, even though we don't care about almost all of it's properties:
SpecBegin(SomeClass)
__block SomeClass *subject;
before(^{
User *user = [[User alloc] init];
user.firstName = @"Bill";
user.lastName = @"Smith";
user.friendCount = 7;
user.title = @"Mr";
user.maidenName = @"Bloggs";
subject = [[SomeClass alloc] initWithUser:user];
});
it(@"is valid", ^{
expect([subject isValid]).to.beTruthy();
});
});
This setup code begins to dominate even the simplest of tests, and as we start to write more tests requiring User
s, we end up writing a ModelFixtures
class to DRY it up a little bit:
SpecBegin(SomeClass)
__block SomeClass *subject;
before(^{
User *user = [ModelFixtures user];
subject = [[SomeClass alloc] initWithUser:user];
});
it(@"is valid", ^{
expect([subject isValid]).to.beTruthy();
});
});
This is OK for a while, but we realise then that we need multiple slightly different fixtures for different tests. The ModelFixtures
class then ends up littered with extra methods to help like this:
@interface ModelFixtures : NSObject
+ (User *)user;
+ (User *)userWithFirstName:(NSString *)firstName;
+ (User *)userWithFirstName:(NSString *)firstName
lastName:(NSString *)lastName;
+ (User *)userWithFirstName:(NSString *)firstName
lastName:(NSString *)lastName
maidenName:(NSString *)maidenName;
@end
With FactoryGentleman, you define the object's base fields in one file, and later build the objects, together with any overridden fields you wish.
Create an implementation file (*.m) with the factory definition:
#import <FactoryGentleman/FactoryGentleman.h>
#import "User.h"
FGFactoryBegin(User)
builder[@"firstName"] = @"Bob";
builder[@"lastName"] = @"Bradley";
[builder field:@"friendCount" integerValue:10];
builder[@"title"] = @"Mr";
builder[@"maidenName"] = @"Macallister";
FGFactoryEnd
#import <FactoryGentleman/FactoryGentleman.h>
#import "User.h"
SpecBegin(User)
__block User *subject;
before(^{
subject = FGBuild(User.class);
});
it(@"is valid", ^{
expect([subject isValid]).to.beTruthy();
});
});
You can override fields when building the objects by passing either the builder block:
#import <FactoryGentleman/FactoryGentleman.h>
#import "User.h"
SpecBegin(User)
__block User *subject;
context(@"when user has no first name", ^{
before(^{
subject = FGBuildWith(User.class, ^(FGDefinitionBuilder *builder) {
[builder nilField:@"firstName"];
});
});
it(@"is NOT valid", ^{
expect([subject isValid]).to.beFalsy();
});
});
});
or by passing a dictionary of the values:
#import <FactoryGentleman/FactoryGentleman.h>
#import "User.h"
SpecBegin(User)
__block User *subject;
context(@"when user has no first name", ^{
before(^{
subject = FGBuildWith(User.class, @{ @"firstName" : @"" });
});
it(@"is NOT valid", ^{
expect([subject isValid]).to.beFalsy();
});
});
});
You can define a field with some more complex state-dependent values using blocks:
#import <FactoryGentleman/FactoryGentleman.h>
#import "User.h"
FGFactoryBegin(User)
__block int currentId = 0;
[builder field:@"resourceId" by:^{
return @(++currentId);
}];
builder[@"firstName"] = @"Bob";
builder[@"lastName"] = @"Bradley";
[builder field:@"friendCount" integerValue:10];
builder[@"title"] = @"Mr";
builder[@"maidenName"] = @"Macallister";
FGFactoryEnd
You can define objects with immutable (i.e. readonly) properties via listing initializer selector together with the field names needed:
#import <FactoryGentleman/FactoryGentleman.h>
#import "User.h"
FGFactoryBegin(User)
builder[@"firstName"] = @"Bob";
builder[@"lastName"] = @"Bradley";
[builder field:@"friendCount" integerValue:10];
builder[@"title"] = @"Mr";
builder[@"maidenName"] = @"Macallister";
[builder initWith:@selector(initWithFirstName:lastName:) fieldNames:@[ @"firstName", @"lastName" ]];
FGFactoryEnd
You can define associative objects (objects that themselves have a factory definition) by giving in the name of the factory required:
#import <FactoryGentleman/FactoryGentleman.h>
#import "User.h"
FGFactoryBegin(User)
builder[@"firstName"] = @"Bob";
builder[@"lastName"] = @"Bradley";
builder[@"friendCount"] = @10;
builder[@"title"] = @"Mr";
builder[@"maidenName"] = @"Macallister";
[builder field:@"address" assoc:Address.class];
FGFactoryEnd
For objects with different traits, you can define them within the base definition:
#import <FactoryGentleman/FactoryGentleman.h>
#import "User.h"
FGFactoryBegin(User)
builder[@"firstName"] = @"Bob";
builder[@"lastName"] = @"Bradley";
builder[@"friendCount"] = @10;
builder[@"title"] = @"Mr";
builder[@"maidenName"] = @"Macallister";
[builder field:@"address" assoc:Address.class];
traitDefiners[@"homeless"] = ^(FGDefinitionBuilder *homelessBuilder) {
[homelessBuilder nilField:@"address"];
};
FGFactoryEnd
These can then be used using the corresponding build macros:
subject = FGBuildTrait(User.class, @"homeless");
subject = FGBuildTraitWith(User.class, @"homeless", ^(FGDefinitionBuilder *builder) {
builder[@"firstName"] = @"Brian";
});
as well as the associative definition:
[builder field:@"user" assoc:User.class trait:@"homeless"];
You can define values for readonly properties (non-primitive), although this
functionality is not available by default. In order to set them you'll have to
define FG_ALLOW_READONLY
in your prefix header.
Add pod "FactoryGentleman"
to your Podfile
FactoryGentleman is available under the MIT license. See the LICENSE file for more info.