Why do we need test code?
I recently created some test code for an Angular project. It was my first time learning how to test but I realized how important it was because of how much our team can be put at ease knowing that all important tests pass. We can be put at ease because we know our project will work according to how we want it even if we add new features to our project. This is my personal opinion but I think that if your project is not changing and will stay the same forever there is no need to add test code to your project. It is most useful when your project is constantly evolving or improving in some way.
Angular provides Jasmine, a testing framework, out of the box which is why our team used it. But, I believe, the overarching concepts among all different testing frameworks are similar; so, getting one down would help you easily transition into different testing frameworks. Now, let's get into what these overarching concepts are and how I implemented them in my project.
What is testing exactly?
I think everyone can intuitively somewhat guess what testing is. Basically, testing is checking(or testing) to see if our code works the way we want it to in different situations. Now the hard part is actually implementing these concepts which I will go over below.
There are different types of testing: unit, integration, and e2e(end-to-end). This post will go over unit testing because it is the most commonly used and a great starting point. Now, what is unit testing? Unit testing is basically testing only the unit and excluding all dependency injections("DIs"), child components, and all other related things. This helps pinpoint the problem when there is one. For example, if there are two components called parentComponent and childComponent and you are testing parentComponent, then you would exclude childComponent from the test. How do you do that? That's the hard part.
How do you do unit testing?
A component is usually pretty useless without its DIs, child components, and etc. So it was hard for me to wrap around how you can test a component without its dependencies. But basically, you have to make fake DIs, child components, and etc. For example, if your actual project has a service to asynchronously get some data from somewhere you would have to create a fake service or as called in Jasmine a "spy" to replace that service that the component depends on.
I won't go over everything that I did in the project because I don't think it'll be too useful for everyone but I do think there are three main difficulties I faced that everyone will to some degree face as well when writing test code.
What are the three main difficulties?
- Learning how to deal with asynchronous functions
- Learning how to make fakes(or stubs) for components, DIs, and etc.
- Understanding the whole process of testing
Understanding the whole process of testing
Let's go over the easiest one of the three, understanding the whole process of testing including just getting used to the new syntax. There are methods like "describe", "beforeEach", "it", "expect", etc. which are methods provided in Jasmine. Let's go over those four methods because it will give the general idea of how test code works.
- "describe" method("suite"): this is basically where you put in all your test code and is used to group related specs
- "it" method("spec"): this is a spec within the suite
- "beforeEach" method: this runs before each spec method
- "expect" method: you expect the specs to have a certain value or do something
I'm sure this makes no sense at all. Let's go over an example. Let's say that when a search function is called we want a spinner show method to have been called. This situation in test code would look like the example below.
let component: ParentComponent;
describe("parentComponent", () => { //this is the suite
beforeEach(() => {
component = fixture.componentInstance;
});
it('should show the spinner when the component is loading', () => {
component.search(); // run the search function in the component
expect(component.spinner.show).toHaveBeenCalled();
//You expect the "show" method in the spinner to have been called after running the search function in the component
})
}
It really depends on how you implemented your spinner in your project, but in mine the spinner has a show method that is called when the component search function is called.
Learning how to make fakes(or stubs)
Fakes are also called stubs, spies, mocks, and etc. I think there are some differences but I will be using them interchangeably for convenience purposes.
In testing, you basically have to make stubs for everything. If a component has a child component, a dependency injection, or anything else that's no within the component we're testing then just think that a stub needs to be made.
But, I do think this part, making stubs, is where the architecture of Angular really shines. Unlike Vue or React, Angular is composed of modules and uses dependency injections to separate the view (component) from the data processing(services) functionality . It is really easy to know which dependencies you need for each component making it easier to know which stubs you need to create.
In this post I will go over how you can create stubs 1)for services or dependency injections and 2)for values that should be return as a result of calling a method.
describe('IssuesComponent', () => {
let component: IssuesComponent;
let fixture: ComponentFixture<IssuesComponent>;
beforeEach( waitForAsync(() => {
await TestBed.configureTestingModule({
declarations: [ ParentComponent ],
schemas:[NO_ERRORS_SCHEMA],
providers:[
{provide: DataService, useValue:jasmine.createSpyObj<DataService>("DataService", ['search'])},
] // 1)this is how you create a spy for a service. you are basically telling Jasmine to use this spy instead of the actual dataservice.
})
.compileComponents();
}));
beforeEach( waitForAsync(() => {
fixture = TestBed.createComponent(IssuesComponent);
component = fixture.componentInstance;
}));
it('should run the search function properly', fakeAsync (() => {
(<any>component).issue.search.and.returnValue(of({
hits:{hits:[], total:{value:3, relation: 'eq'}},
timeTookForSearch:3,
aggregations:{status:{buckets:[]}}
}).pipe(delay(10)) // add delay to make the observable async
) // 2)this part is creating a fake response
// everytime the search function is called it returns the fake value that you tell it to return
I didn't go over how to make stubs for components and many other things but I do think this is a good start.
Learning how to deal with asynchronous functions
We all know that some functions are asynchronous which means that we have to deal with this problem while testing as well. Every time everything seems to be working logically but does not work, the problem, usually, lied in some asynchronous function for me. Jasmine provides tools to test asynchronous functions. The methods are called "fakeAsync" and "tick". "fakeAsync" creates a zone in which we can pass time manually using "tick".
describe("parentComponent", () => {
it('should test async functions', fakeAsync( () => {
let test = false;
setTimeout(() => {
test = true;
expect(test).toBeTruthy();
}, 1000); // you set the timeout to be 1000ms
tick(1000); // tick fast forwards time by 1000ms in this example
}))
})
There's also another method you can use called "flushMicrotasks()" instead of "tick()". You have to understand the callback queue and microtask queue to understand how this works. Check this post to understand how microtask queues work
Basically, tick and flushMicrotasks is the same thing but flushMicrotasks you flush the microtask queue while tick flushes the callback queue.
Top comments (0)