As a pre-requisite, it is expected to have npm and angular cli installed in your machine. The installations are pretty straight forward, you may follow the links and install if you don’t have it in your machine.
The complete code for this can be found in my github url.
Step-1: First step is generate a new angular application. Lets name it as todo-angular.
ng new todo-angular
The CLI will ask for a few questions on whether a stricter type checking is required, angular routing is required and the stylesheet format. Lets stick to the defaults for now.
? Do you want to enforce stricter type checking and stricter bundle budgets in the workspace?
This setting helps improve maintainability and catch bugs ahead of time.
For more information, see https://angular.io/strict No
? Would you like to add Angular routing? No
? Which stylesheet format would you like to use? CSS
Step-2: Once the project is generated, run npm install command from the project folder so that all the project dependencies are downloaded. After this step, if you run ng serve command, angular builds and serves the app that can be reachable at http://localhost:4200 (if port 4200 is already in use, you will be asked if a different port can be used)
Step-3: Now lets generate a new component with name todo. Basically components are the building blocks of an Angular application and our goal here is to create a component that can list all todos. Run the below command
ng g c todo
Alternatively you can also run a more descriptive command – ng generate component todo.
The generate component command will create a folder todo with four files in it.
todo.component.css– The css file for this component. Any styles specific to this component should go in this filetodo.component.html– The component html templatetodo.component.ts– The component class which will be written in TypeScript.todo.component.spec.ts– The test case for the component
Step-4: Lets run the tests now using the below command. The command pops up a new browser window with the test results.
ng test
So far we haven’t added any custom code and so all the tests should be running fine. Note that by default ng test runs in a browser and also watches for any changes in the code. If there are any changes in the code, angular will automatically re-compile the code and re-run the tests.
Step-5: Now lets write a new test in todo.component.spec.ts.
A component, unlike all other parts of an Angular application, combines a HTML template and a TypeScript class. The component truly is the template and the class working together. To adequately test a component, we should test that they work together as intended.
Such tests require creating the component’s host element in the browser DOM and investigating the component class’s interaction with the DOM as described by its template. The Angular TestBed facilitates this kind of testing.
We can already see that the Angular component generator has already generated all the boilerplate code for the test case including the TestBed configurations.
Coming back to our test, as mentioned in step 3, our goal with this component is to list down all todos. Lets assume that we will display all todos in a table and each row in the table corresponds to a single todo. Also lets assume that we will have a CSS class for each row. And lets name the CSS class as todo-item.
So basically our test case should look for the presence of one ore more UI elements with CSS class todo-item. Lets see how the code looks like
it('should show all todos', () => {
let element: HTMLElement = fixture.nativeElement;
expect(element.querySelectorAll('.todo-item')?.length).toBe(2);
});
ComponentFixture is a test harness for interacting with the created component and its corresponding element. Here we are using the nativeElement value from the component fixture which always returns a HTMLElement or one of its derived classes. (I think its worth to read about DebugElement if you are planning to run tests in a non-browser platform). HTMLElement.querySelectorAll method helps us to query the dom to find elements with class todo-item.
Like expected, the test case should fail because we don’t have any such elements in our html template.
Step-6: Lets add the code to make the test pass. Move on to the HTML template (todo.component.html) and add some static todo items to see if the tests are succeeding
<div class="todo-item">Copy</div>
<div class="todo-item">Paste</div>
Now the tests are running fine.
Step-7: Lets generate a model class for the Todo. Run the below command if you want Angular CLI to generate the model class. The flag –skip-tests=true tells Angular CLI not to generate tests for the model. Model class is just a normal TypeScript class that can be created manually as well.
ng g class todo/todo --type=model --skip-tests=true
Lets keep the model (todo.model.ts) simple. So I will add only three properties – id, name and description. Here is how the class looks like
export class Todo {
constructor(
id: string,
name: string,
description: string) {}
}
Step-8: Now lets see how we can connect to a backend service to fetch a list of todos. Lets generate a service for that. Run the below command to generate a todo service inside the todo component folder.
ng g s todo/todo
Alternatively, we can also use the descriptive command ng generate service. If you want to generate the service in a different folder, say services folder, then run the command ng g s services/todo. The above command generates two files – todo.service.ts (service class) and todo.service.spec.ts (test class for the service)
Step-9: Lets add a new test case for the generated service (todo.service.spec.ts). Angular CLI has already generated the boilerplate for the test class. Basically the service should connect to a backend API and fetch all todos.
Lets see how the test case looks like
describe('TodoService', () => {
let service: TodoService;
let expectedResult: Todo[] | null;
let httpTestingController: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
});
service = TestBed.inject(TodoService);
httpTestingController = TestBed.inject(HttpTestingController);
});
it('should return a list of todos', () => {
const todos = [
{
id: '112233',
name: 'Learn',
description: 'Learn at leat one thing a day',
},
{
id: '445566',
name: 'Pratice',
description: 'Practice what you have learned',
},
];
service.query().subscribe((response) => (expectedResult = response.body));
httpTestingController.expectOne({ url: '/api/todos', method: 'GET' }).flush(todos);
httpTestingController.verify();
expect(expectedResult).toEqual(todos);
});
});
Lets assume that there is a query method in the service class which returns an Observable http response with list of todos. Note that Observable type comes from RxJS, which is a library for reactive programming that makes it easier to compose async or callback-based code.
To mock the http call from the service, we have added a HttpTestingController that comes from the HttpClientTestingModule. Note that we should import HttpClientTestingModule in the TestBed configuration for HttpTestingController to work.
The below code imports HttpClientTestingModule in to TestBed configuration and injects a HttpTestingController in to the TestBed.
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
});
httpTestingController = TestBed.inject(HttpTestingController);
The below line sets up the http mocking, i.e., testing controller expects a GET call to /api/todos API and returns a JSON array of todos.
httpTestingController.expectOne({ url: '/api/todos', method: 'GET' }).flush(todos);
In the assertion, we verify if the testing controller was called by the service class (httpTestingController.verify()) and also check if the expected result from the service is the same as returned by the mock test controller.
After this, lets add a query method to our service class so that the build succeeds. Here is the query method without any implementation.
type EntityArrayResponseType = HttpResponse<Todo[]>;
...
query(): Observable<EntityArrayResponseType>{
throw new Error('Method not implemented.');
}
The tests will fail because the method is not implemented and it just throws an error.
Step-10: Lets implement the service – query method (todo.service.ts) now. To perform the backend API call, we will make use of HttpClient that comes in the anguarl/common/http package. Here is the code to make the backend API call
type EntityArrayResponseType = HttpResponse<Todo[]>;
@Injectable({
providedIn: 'root'
})
export class TodoService {
constructor(private httpClient: HttpClient){}
query(): Observable<EntityArrayResponseType>{
return this.httpClient.get<Todo[]>('/api/todos', {observe: 'response'});
}
}
If {observe: ‘response’} is not provided in the httpClient get method call, angular will default the value of observe to ‘body’. In that case, we will get an Observable Todo lst response (Observable<Todo[]>) instead of Observable<HttpResponse<Todo[]>>.
With the above code change, the tests should be running ok.
Step-11: Now we have to call the service from component. Lets write the test case (todo.component.spec.ts) first.
describe('TodoComponent', () => {
let component: TodoComponent;
let fixture: ComponentFixture<TodoComponent>;
let service: TodoService;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
declarations: [TodoComponent],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(TodoComponent);
component = fixture.componentInstance;
fixture.detectChanges();
service = TestBed.inject(TodoService);
spyOn(service, 'query').and.returnValue(of(new HttpResponse({
body:[
{
id: '112233',
name: 'Learn',
description: 'Learn at leat one thing a day',
},
{
id: '445566',
name: 'Pratice',
description: 'Practice what you have learned',
}
]
})));
});
it('should show all todos', () => {
expect(component).toBeTruthy();
let element: HTMLElement = fixture.nativeElement;
expect(service.query).toHaveBeenCalled();
expect(element.querySelectorAll('.todo-item')?.length).toBe(2);
});
});
We have injected TodoService in to the TestBed. And created a spy on the service’s query method. If the query method is called, then a HttpResponse with list of todos will be returned. And in the expectation, we just check if the service.query method to have been called.
Lets add a few more assertions to validate that the values from the component class are available in final DOM.
expect(element.querySelectorAll('.id')[0].textContent).toBe('112233');
expect(element.querySelectorAll('.id')[1].textContent).toBe('445566');
expect(element.querySelectorAll('.name')[0].textContent).toBe('Learn');
expect(element.querySelectorAll('.name')[1].textContent).toBe('Practice');
expect(element.querySelectorAll('.description')[0].textContent).toBe('Learn at leat one thing a day');
expect(element.querySelectorAll('.description')[1].textContent).toBe('Practice what you have learned');
So far we haven’t implemented the code to call the service or we haven’t changed the html template to use the values from the service, so the tests should fail.
Step-12: Let call the service from the component (todo.component.ts) now.
export class TodoComponent implements OnInit {
todoList: Todo[];
constructor(protected todoService: TodoService) { }
ngOnInit(): void {
this.todoService.query().subscribe((response: HttpResponse<Todo[]>) => {
this.todoList = response.body;
});
}
}
Inject TodoService as a constructor param and subscribe to todoService.query response. On success response, set the response body to todoList property of component class. After this change, the below expectation will succeed. But still there are other assertions which will fail since the HTML template is static and hard coded.
expect(service.query).toHaveBeenCalled();
Step-13: Change the html template (todo.component.html) to use the values from the component class. Lets keep the template very simple not to loose our focus. We can look at CSS and styling in the future chapters.
<table>
<thead>
<th>ID</th>
<th>Name</th>
<th>Description</th>
</thead>
<tbody>
<tr class="todo-item" *ngFor="let item of todoList">
<td class="id">{{item.id}}</td>
<td class="name">{{item.name}}</td>
<td class="description">{{item.description}}</td>
</tr>
</tbody>
</table>
Here we have a table with header and body. In the body, we iterate through each item in the todoList and display the item’s id, name and description in the table.
Now the tests for todo-component should be working fine and the todo-component is ready to be glued to any other component. In this case, lets add the todo-component to our main app component.
Step-14: Add test case to use the todo-component in the main app component.
const compiled = fixture.nativeElement;
expect(compiled.querySelector('app-todo')).not.toBe(null);
Step-15: Add the todo-component to the main app component. Replace the html in app.component.html with the below code
<app-todo></app-todo>
That’s it. All tests are running fine and our application is ready to use. If you run an ng serve now from the application folder, we should be able to see a running application that displays a table of todos.
Note that there are chrome plugins available to simulate the behavior of backend APIs. For example, tweak mock API is a chrome plugin that helps t configure mock API calls for testing purpose.
When running the application, I noticed that the HTTP calls were not working like expected and to fix that I had to import HttpClientModule in app module configuration (app.module.ts) so that HttpClient dependency is available for all classes in the application.
@NgModule({
declarations: [
AppComponent,
TodoComponent
],
imports: [
BrowserModule,
HttpClientModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
I hope the post was informative, we will see the implementation of other CRUD operations in the coming chapters.
Please refer my github for the complete code. Its possible that I would have missed some little configuration or code, so it is always best to go with the github repo.
Also feel free to share your comments and feedback if any.