The TDD approach might be:
it('should remove the last element in the array')
whereas the BDD approach might be described like
it('should remove the last grocery item added')
Both test the same thing but the TDD approach has knowledge of the internals of the implementation. BDD tests the desired behavior.
The configuration of the test in the file test.ts
has ten steps to follow (in this app):
-
Import required dependencies
import './polyfills.ts'; import 'zone.js/dist/long-stack-trace-zone'; import 'zone.js/dist/proxy.js'; import 'zone.js/dist/sync-test'; import 'zone.js/dist/jasmine-patch'; import 'zone.js/dist/async-test'; import 'zone.js/dist/fake-async-test'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { getTestBed, TestBed } from '@angular/core/testing'; import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; import { App, Config, Form, IonicModule, Keyboard, DomController, MenuController, NavController, Platform } from 'ionic-angular'; import { ConfigMock } from './mocks';
-
Declare the not yet available typing for Karma
declare var __karma__: any; declare var require: any;
-
Prevent Karma from running prematurely
__karma__.loaded = function (): void { // noop }
-
Initialize the Angular testing environment:
getTestBed().initTestEnvironment( BrowserDynamicTestingModule, platformBrowserDynamicTesting(), );
-
Find all the tests. with
let context: any = require.context('./', true, /\.spec\.ts/);
-
Load the modules.
context.keys().map(context);
-
Start Karma to run the tests
__karma__.start();
A unit test itself has more or less this shape:
- Import all required dependencies. Even the providers used in the specific component (e.g. StatusBar or SplashScreen)
- Add variables for comp, e.g
let comp: MyApp;
, and fixtures, e.g.let fixture: ComponentFixture<MyApp>;
- Write the test suite with
describe() {}
- In the test suite write the beforeEach() function with the async parameter (this wraps a function in an asynchronous test zone. The test will automatically complete when all asynchronous calls within this zone are done.) and its callback function with
TestBed.configureTestingModule()
for configuration of the TestBed (with 'declarations', 'providers' and 'imports'):This allows overriding default providers, directives, pipes, modules of the test injector, which are defined in test_injector.js(method) TestBed.configureTestingModule(moduleDef: { providers?: any[]; declarations?: any[]; imports?: any[]; schemas?: (any[] | SchemaMetadata)[]; }): typeof TestBed
- Resolve the Promis with
.compileComponents()
- it's necessary as fetching urls is asynchronous. - Write beforeEach() function for all things to do upfront the tests, e.g.
beforeEach( () => { fixture = TestBed.createComponent(MyApp); comp = fixture.componentInstance; });
- Write a afterEach() function for destroying the things setup in the beforeEach() funktion, e.g.
afterEach( () =>{ fixture.destroy(); comp = null; });
- Now writing the test(s) starts with a kind of shape like this:
or like this
it( 'is created', () => { expect(fixture).toBeTruthy(); expect(comp).toBeTruthy(); });
it( 'displays the product page to the user', () => { expect( comp['rootPage'] ).toBe(ProductPage); });
That's it.
- Write a test
- Run the test (it should fail)
- Write the code to satisfy the test
- Test again (it should pass)
- Refactor as necessary
Beginner example of writing a test for a static "service" (provider in Ionic speak), the starting point to a real world service with e.g. HTTP requests
- Import all necessary dependencies:
import { Products } from './products'; import { NavController, NavParams } from 'ionic-angular';
- Declare a variable for the instance of the service which should be tested:
let productsService;
- Write the test suite function:
describe('Provider: Products', () => { // // beforeEach() ... // // it()... // //... });
- In that test suite write the
beforeEach()
function:beforeEach(() => { | // This instatiation only works because the actual service | // has no dependencies (e.g. Http). With dependencies TestBed \|/ // is needed to inject these dependencies V productsService = new Products(); });
- And the
it
function - the test case - as well:it('should have a non empty array called products', () =>{ let products = productsService.products; expect(Array.isArray(products)).toBeTruthy(); expect(products.length).toBeGreaterThan(0); })
- Done
In this test for the service the provided Http won't be used in its normal form. The implementation will be modified by using useFactory
. By that only the service will betetsted and not the external service as an isolated case.
The useFactory
uses a service provided by Angular called MockBackend
. This is for testing purpose and alows to create a fake backend which send fake responses. These responses are still performed asynchronously, like a normal HTTP request would be.
When mocking the backend like this, any response can be used for testing. In this case a JSON
string is used hard coded. The mock response is setup like this:
<pre><code>
mockBackend.connections.subscribe( (connection) => {
connection.mockRespond( new Response( new ResponseOptions( {
body: mockResponse
})));
})
</pre></code>
This has effects on the test of the products page, too. With the simple test mentioned above the Products service is referenced and checks its data immediately. but the data isn't available immediatle, becaus it's loaded asynchronously. It would make the former test fail. Now the correct approach should isolate the unit test as much as possible. A mock for the Products service is needed, so this specific component will only be tested.
A ProductsMock is needed in src/mocks:
<pre><code>
export class ProductsMock {
public products: any = [
{ "title": "Cool shoes", "description": "Isn\'t it obvious?", "price": "39.99" }
]
}
</pre></code>
And this mock has to be imported into product.spec.ts
and declared in the providers array like so:
<pre><code>
{
provide: Products,
useClass: ProductsMock
}
</pre></code>
This takes out the provided service and follows the approach to fake the HTTP backend and swap the real Products with a fake ProductsMock by using useClass
.
(actually there's only one but I think more to come ;-)
-
Compilation errors in @types/jasmine v.2.5.42
=> Update TypeScript to newer version e.g. v2.2.2