Introduction to Specta/Expecta

Mark Edward Murray
6 min readApr 17, 2015

--

This week I dove into writing my own unit tests from scratch. While it’s true that I’ve been exposed to them consistently over the previous ten weeks of Flatiron’s program, I hadn’t yet attempted to manufacture my testing suite out of empty files. I did a number of things wrong in the process so here’s a quick guide with the intent of keeping others from getting stuck on some of those same things.

Before I get to showing code samples, let’s go over the set up. First, open your Podfile and add the ‘Specta’ and ‘Expecta’ pods to the ‘<fileName>Tests’ target and run ‘pod install’ in your terminal.

// PodfileTarget ‘FISCardStreams’ do
end
Target ‘FISCardStreamsTests’ do
pod ‘Specta’
pod ‘Expecta’
end

Make sure you now continue working in the *.xcworkspace file.

Next, in XCode, with the Alcatraz Package Manager already installed, add the Specta Templates package by Luiza-Cicone. This will generate our *Spec.m files with the correct targets and some boilerplate code.

Now we’re ready to generate the test file. In our case, this is going to be a Spec for the FISComment custom class, so let’s create our FISCommentSpec file using the template. Create a New File with the following steps:

Select the Specta template added by the Specta-Templates-Xcode package. Select “Next”.
Enter the class you wish to test. Select “Next”.
(default) Select ONLY the <filename>Tests target at the bottom of the next window. Select “Create”.

This new FISCommentSpec.m file load with the following boilerplate code:

#import “Specta.h”
#import “FISComment.h”
SpecBegin(FISComment)describe(@”FISComment”, ^{

beforeAll(^{
});

beforeEach(^{
});

it(@””, ^{
});

afterEach(^{
});

afterAll(^{
});
});
SpecEnd

Unfortunately, the headers aren’t set for using Expecta yet. We need to import it and define the shorthand:

#import <Foundation/Foundation.h>
#import <Specta/Specta.h>
#define EXP_SHORTHAND
#import “Expecta.h”
#import “FISComment.h”

The “SpecBegin(<className>)” and “SpecEnd” are wrappers similar to the “@implementation” and “@end” in your class files and the compiler will complain if these are interrupted or missing.

The “it(NSString *name, ^{ … });” blocks are the actual test containers with a string that describes specifically what the test does.

The “describe(NSString *name, ^{ … });” blocks are wrappers that prepend a string to the displayed name of all the tests (or “it” blocks) within it. These “name” strings are very useful in self-documenting what each test is intended to check for and what its scope is.

The “beforeAll(^{ … });”, “beforeEach(^{ … });”, “afterAll(^{ … });”, and “afterEach(^{ … });” blocks are for setting up (or resetting) the instance variables to test by and the instances of the test class to run tests on. Keep in mind that “beforeAll” and “afterAll” only get dispatched once for all of the tests that they contain, but “beforeEach” and “afterEach” get newly run for every “it” test block.

Since one of the great features of Expecta is its asynchronous nature, it’s generally advisable to use “beforeEach” in order to start each test fresh with the same data since “beforeAll” only gets called once. The “afterAll” and “afterEach” blocks aren’t typically needed unless resetting some outside class or user default. We won’t be using them here.

True to Test-Driven-Development (TDD) form, let’s start by writing some tests for FISCommentSpec. Let’s expect to write a designated initializer that sets all of the properties it needs, and a default initializer that calls the designated initializer to pass in non-nil objects.

Let’s declare the properties we want FISComment to have, and the instance names that we want to generate:

SpecBegin(FISComment)
describe(@"FISComment", ^{
__block NSString *commentID;
__block NSString *parentID;
__block NSDate *createdAt;
__block NSString *content;
__block FISComment *defaultComment;
__block FISComment *designatedComment;
});
SpecEnd

You’ll notice that we need to preface these declarations with “__block” (Note: this is a double-underscore). This places all of these objects within the scope of every test block in the current file. Now let’s set them in “beforeEach”:

SpecBegin(FISComment)
describe(@"FISComment", ^{
{ ... } beforeEach(^{ commentID = @”commentID”;
parentID = @”parentID”;
createdAt = [NSDate date];
content = @”content”;
defaultComment = [[FISComment alloc]init];
designatedComment =
[[FISComment alloc] initWithCommentID:commentID];
});
});
SpecEnd

And now we can next some describe blocks for the methods we’re going to write. Make sure you close out the “beforeEach” block before continuing:

SpecBegin(FISComment)
describe(@"FISComment", ^{
{ ... } beforeEach(^{ { ... }
});
describe(@"default initializer", ^{
});
describe(@"designated initializer", ^{
});
});
SpecEnd

Now let’s add some tests (“it” blocks) and test conditions:

SpecBegin(FISComment)
describe(@"FISComment", ^{
{ ... } beforeEach(^{ { ... }
});
describe(@"default initializer", ^{
it(@"sets all properties to default values", ^{
expect(defaultComment.commentID).to.equal(@"");
expect(defaultComment.parentID ).to.equal(@"");
expect(defaultComment.createdAt).to.
beKindOf([NSDate class]);
expect(defaultComment.content ).to.equal(@"");
});
});
describe(@"designated initializer", ^{
it(@"sets all properties to submitted values", ^{
expect(designatedComment.commentID).to.equal(commentID);
expect(designatedComment.parentID ).to.equal(parentID);
expect(designatedComment.createdAt).to.equal(createdAt);
expect(designatedComment.content ).to.equal(content);
});
});
});
SpecEnd

The readability of the condition lines is much of makes Expecta so great. Begin the line with the “expect(…)” which encapsulated the instance variable you are testing — in our case, each of the properties of a FISComment object. The “.to” is actually not required for the positive, but is a helpful syntactic tool that also acts a placeholder for the inversion specifier written as either “.toNot” or “.notTo”.

The final element is the match specifier — how the instance variables are being compared — which encapsulates the instance variable we are testing. In this example we are using “.equal(…)” and “.beKindOf(…)”, but there is an extensive list of built-in matchers detailed in Expecta’s documentation. Expecta is written to allow custom matchers to be written quickly, but in most cases, the built-in matchers can be applied.

At this point, we should run our tests to check that they fail. This important to make sure that you have not written tests that always pass and are thereby useless. This is the “Red” step in the Red-Green-Refactor TDD mantra.

Now let’s pursue those green lights by writing out our FISComment class to match our test’s expectations. We need it to have four properties, a designated initializer, and an overridden default initializer:

FISComment.h@interface FISComment : NSObject@property (strong, nonatomic) NSString *commentID;
@property (strong, nonatomic) NSString *parentID;
@property (strong, nonatomic) NSDate *createdAt;
@property (strong, nonatomic) NSString *content;
- (instancetype)init;- (instancetype)initWithCommentID:(NSString *)commentID
parentID:(NSString *)parentID
createdAt:(NSDate *)createdAt
content:(NSString *)content;
@end
FISComment.m
#import “FISComment.h”@implementation FISComment- (instancetype)init {
self = [self initWithCommentID:@””
parentID:@””
createdAt:[NSDate date]
content:@””];
return self;
}
- (instancetype)initWithCommentID:(NSString *)commentID
parentID:(NSString *)parentID
createdAt:(NSDate *)createdAt
content:(NSString *)content {
self = [super init];
if (self) {
_commentID = commentID;
_parentID = parentID;
_createdAt = createdAt;
_content = content;
}
return self;
}
@end

Now we can run our tests and see if they pass. If they do, we’ll see a list of green lights in the navigation window.

This is concludes the “Green” phase. Now that we have the ability to check whether changes to our code break our FISComment class methods, we can graduate to the “Refactor” phase. In our case, we’ve already written two very solid initializers so we don’t have anything to refactor in FISComment.

--

--