TDD using Quick & Nimble
This post demonstrates some example usage of Quick & Nimble within TDD process. This post is aimed at anyone who would like to use the TDD process, but does not have a clue how to kickoff. I will start with a brief overview of the TDD using Quick & Nimble. If you are already familiar with it, hop on over to the example.
xCode version: 8.2
Swift version: 3.0.2
Pre-requisities:
Basic knowledge of Carthage dependency manager and its usage.
What is TDD (Test driven development) ?
Test driven development is a process of 3 tightly bound activities. Testing, which should be executed first, coding (to supply the tests with working and testable code) and refactoring. Refactoring should play a significant role in this process as it forces you to think about your code in the way of simplicity, usability and testability.
Key steps to follow when using TDD
- Identify the testables
Write your test class in this step. - Define the implementation stubs for the objects from #1
You need to represent the testables and create empty implementations of them. - Identify the expected outcome and matching conditions of the test.
- Describe the circumstances under which the outcome can be achieved.
Mock the behavior using stubs from step #2. If you are not able to mock it, you should probably add more empty stubs. Go back to step #2. - Setup the test assertions and fill them into the test.
Use the expected outcome and matching conditions from step #3 - Run the test
Failure should occur. If not, try to figure out the problem. Are you really testing anything? Does the test even make sense? - Implement the stubs from #2 adding any necessary method bodies, variables, and behavior.
- Run the test
It should pass. If not, check your code. - Refactor the code in the way of simplification. Rename the classes to be more descriptive, update the test to be more descriptive, remove any boilerplate code, unused variables, forgotten comments .. etc.
Do not forget to rerun the test after any changes that can affect the test result.
Check the agile alliance website for more info on the TDD. Agile alliance refers to common pitfalls and skill levels of the TDD.
Quick & Nimble
You may wonder, why these two? Why not actually use the built in XCTest
framework?! In fact this combination is simply easy to use and you can define the expected behavior in a more simple way than using XCTest.
Quick is a testing framework while Nimble is a matching framework. I fell in love with these since both are easy to use and really straightforward.
Comparing XCTest to Quick & Nimble
XCTest is a testing framework provided by Apple. It is also pretty straightforward but you need to be aware of setup() and tearDown() functions. XCTAssertions are also pretty inconvenient to use. If you take a look at the naming of the test methods, it is also kind of complicated to descriptively capture the test case not having very long function names.
Quick is a testing framework which provides a convenient method to compose a unit test. It uses the single function spec() to define the whole test. The spec() function is composed of multiple sections. The sections are describe, context and it. I will explain the purpose and benefits of such a setup later in this article.
Nimble is a matching framework which provides plenty of options how to fulfill your test expectations. The keyword expect is very important, because it replaces XCTAssertion of XCTest.
expect(XCTestAssertion.isHard(to: [.maintain, .read, .write])).to(beTrue())
Nimble provides a lot of matchers which will make your life writing unit tests way easier! As you can see in the above example, Nimble chains expect to to allowing you to read the statement as one sentence. I consider this fact as a huge benefit over XCTest.
Summary of Quick & Nimble pros over XCTest:
- Type safety
Nimble uses generics in swift! - Asynchronous evaluation
Sometimes you just need to test the outcome of an asynchronous operation. Nimble is perfect for such a case. All you need to do is to combine expect() with toEventually() and your case to be continuously re-evaluated for 1 second. If you expect the condition to match later on, you can specify timeout in toEventually() - Wait until
It does exactly what it says on the tin. The queue is blocked until the callback of waitUntil is called. - Multiple matching options
String comparisons, beginWith, endWith …
Checking if NSNotification was sent is also supported.
Collections evaluation, type checking and many many more. - Extendibility
You can write your own matchers if you want.
This is just a small variety of things that Quick & Nimble is capable of. If you still lack faith in this, I still recommend trying it. You can compare a simple test case and its readability using XCTest vs. Quick & Nimble.
As you can see in the files above, there is a significant difference in test composition. XCTest uses an approach where each method is considered as a test case. Quick & Nimble considers it as a test case.
Did you notice the test case naming? By using XCTest, the function names describe the whole test, right? Using Quick & Nimble is the case composed of describe, context and it!
As you can see, the test case is composed of a whole sentence and thus if it fails, it is easy to identify the root cause.
Set the name of the test case to be descriptive and try to read it as a whole sentence. If it doesn't make sense, consider some changes!
Describe exactly describes what component you are testing.
A Vehicle.
Context describes the purpose of the test or the current state of an object.
After being properly initialized as a Car. (You can and definitely should create tests that will test the inappropriate state of an object.)
It describes the expected result of the test. This is the exact test case.
Should be driveable. (Any expectations can be defined)
As per the above definition, the object should be declared in the context section. Its state should either be modified in the context or it section and expectations should match in the it section. Always remember that top-down structure is used and the resources declared in the describe section are shared among the contexts.
If you are about to start with testing, try to start with real world examples. These are simple to distinguish and you can measure the quality of the test on your own.
Summary
Always try to follow simple rules.
- Does the test even make sense? Does it fail during the first attempt?
- What is the object I am about to test?
- What is the behavior I am about to test?
- What should be the outcome of such behavior? — The final state of the tested object.
References
XCTest: https://developer.apple.com/reference/xctest
Quick: https://github.com/Quick/Quick
Nimble: https://github.com/Quick/Nimble
Real world example
Every child has played with colors before. Lets solve the following task.
You have a color palette and your task is to mix the colors in the way that the outcome is green. Remember what you have learned so far and try to use the TDD approach. [Text solution at the end of post]
Example
Imagine that you are about to develop a module which should represent the vehicle and you know that the following should be accomplished:
- It should be driveable
- It should have 1 brand per vehicle
- It can represent multiple types (Car, FastCar, etc. …)
- It should have a particular number of wheels based on the vehicle type
- Maximum speed is variable based on the car type
Alright, lets setup the environment. Create a new xCode project, choose Single view application
and check Include unit tests.
XCode will create a new project for you including 2 targets. Go to your project settings, choose Application
target, change the bookmark to Build Settings
and search for Defines module
under Packaging.
Set this option to Yes.
You need to check this because the application target does not define the module by default, only framework does.
In the project navigatior, go to Tests
group and delete the default test because we do not need it anymore.
It's time to link Quick & Nimble to our project. You can choose, how to manage the dependencies. There are many possibilities from which I have chosen Carthage. Create the Cartfile and add Quick & Nimble as dependencies. Download, install and build the dependencies. Link the dependencies to your project. If you are not familiar with the process, you should read the guide on the official github page.
Add the copy files phase setting destination as Frameworks.
If you do not perform this step, you will get an error saying:
Could not find binary Quick, could not find binary Nimble
Try to build the project, there should be no errors. Once the project setup is complete we can proceed to testing all the knowledge gained so far.
Define the test
To define the test, get back to your initial task. You need to distinguish what to test.
To identify the testables, define happy path for the test.
Happy path testing is a well-defined test case using a known input, which executes without exception and produces an expected output.
You should also test the corner cases such as nullability and expected behavior when an error occurs. It isn’t just enough to test the happy path.
Not all the requirements are candidates for a unit test. Some of them are constraints for code design. The fact that we need a driveable vehicle, which has a particular number of wheels, is easily identifiable. Moreover the maximum speed is also variable based on the vehicle type.
Try to use as simple declarations as possible at the beginning. Refactoring is one of the key parts of the process and it may improve the code with every iteration.
Start with the test which will test Vehicle
thus name it VehicleSpec.
Leave the implementation empty for now.
It is time to define the content of the test. Fill in all the cases that were previously identified. The vehicle that:
- is driveable.
- has a particular number of wheels based on its type.
- has its maximum speed based on its type.
Can you see the constraints and the relationships among them?
The test is filled with it statements based on the previously set requirements. The project will throw compile time error because there is neither Vehicle class with init function nor Car class. To fix it, create representative classes for Vehicle and Car.
As you can see, we defined Vehicle, Car, Driveable protocol and BrandType enum.
There is another issue with our test so far. We can’t effectively run the test as we do not have any test cases defined yet. However in the previous step, we added representative stubs. We can update the test cases using the above mentioned stubs.
You should end up with all the tests cases covered and the model classes should be enhanced to full functionality. The approach is as straightforward as it can be. Our aim is to cover all test cases defined in our task and I think it should be easy to complete the remaining test cases on your own.
Compare the previous test definition with the final test and you realize that there are small changes. It is due to implementation details that can slightly differ from the original expectation. However, the test still has to be valid and test cases shouldn't be changed.
Alright, we are all green and it means that the test is passing and the first iteration is almost done. However, there is something that isn't as ideal as it could be, right?
Remember, do NOT write useless test cases or tests. A useless test is a bad test and it adds complexity to the whole project. If the test passes upon its creation, it's useless!
Vehicle conforms Driveable by design. So why even test it? Here is a completely useless test case which can be deleted. This is the refactoring process done right after the test is well defined. Rerun the test! There shouldn't be any errors and it should pass. So far so good.
What about the code structure?! We solved the problem by using a very simple solution, but the fastest and simplest solution isn't always good enough, right? We should refactor the code to make the solution cleaner, readable, stable and even maintainable! Swift generics with combination of state design pattern could be a good start to define Vehicle and its Driveable ability.
This is your task to clean up the solution. Give it a try and learn as much as possible from your failures during the test composition. For the ones who are confident in testing and want to compare their approach or whole solution, refer to the github project which supports this post.
Real world example solution
My task is to create a green color using a mixture of colors. Lets define the test.
What do I need? A palette of colors
Define the class for test — ColorsPaletteSpec
class ColorsPaletteSpec: QuickSpec {
override func spec() {
}
}
Ok but wait, I do not have such a class in my project. Create a class named ColorPalette. It's just an empty class with init method.
What do I test ? A color palette
Define the describe part — describe(“A color palette”)
describe("A Color palette") {
let colorPalette = ColorPalette()
}
Huh, ok, but I can’t mix anything now. Lets setup a method called mix in ColorPalette which will optionally return Color. — No parameters.
But we do not have a Color class, lets create one.
What mixture of colors would give me a green outcome? Yellow & blue
Define the context part — context(“after mixing yellow and blue”)
Ok, now we need to add Yellow and Blue colors. Lets create these classes by subclassing the Color.
context("after mixing yellow and blue") {
var color = colorPalette.mix() // currently of type Color
}
What should be the outcome? Green color
Ok lets define the it part — it(“should produce green color”)
You should also create the GreenColor class since we do not have it in our project. Define the expectation of the mix function, so that its resulting color is GreenColor.
it("should produce green color") {
let greenColor = color as GreenColor expect(greenColor).toNot(beNil())
}
Try to run the tests.
It fails. Excellent, lets do some coding.
Complete the ColorPalette class where you can add a list of colors within the init function. Store the list as a property to the class.
Try to run the tests. Compile time error now?! Excellent! Lets do some refactoring.
Fix the problem with the ColorPalette initialization.
Lets have a look at the mix function we defined at the beginning. We called it mix, but it doesn't mix anything, however it should return Color. Refactor the method to mix two colors resulting in a single color.
func mix(_ color1: Color, _ color2: Color) -> Color
Try to run the tests. It fails. Excellent! Lets do some coding and refactor the method. You have to specify the colors as parameters. Create instances of GreenColor and YellowColor and pass them as parameters. You can try to run the test again, but it fails.
Next we need to complete the implementation of the mix function. The implementation is up to you. After proper implementation of the mix function it will pass the whole test.
Once you will get a positive result you can proceed with code refactoring. Try to run the tests after significant changes where you expect the tests can fail.