In the past two tutorials on E2E testing in Ionic (and Angular), I’ve introduced the concept of End-to-End testing and some ways that you can better structure your E2E tests.
If you are new to E2E testing you will likely understand at this point that an End-to-End test can test your application in ways that reflect how the user will actually use it. This might be things like:
- The user can select a product and add it to their cart
- The user can log in and change their account information
- The user can navigate to the Checkout page
Which is different to a unit test which only tests a small isolated “unit” or “chunk” of code, which might be something more like: the getData function should return an array.
But how do we decide what to test in an E2E test? Should we just create a bunch of random tests based on how we think someone would use the application? Should we only worry about E2E tests once the application is completed and we want to verify that it works under certain circumstances?
There’s no correct answer to that question, different people will have different opinions on how it should be done. I think it makes the most sense to do what makes sense to you and your team, and what you think will best test your application. If you don’t already have some sort of preconceived idea of how automated testing should be conducted, then it is quite difficult to figure out where to start.
With that in mind, I’d like to show you how I typically go about integrating E2E tests into my workflow. If you’ve read my series on Test Driven Development in Ionic then you may know that I a particularly like the Test Driven Development (TDD) approach. I’d recommend reading those posts if you are unfamiliar with the concept of TDD, but the general idea is to write tests for your code before you write the code itself. The tutorials I wrote were in the context of unit tests, but that approach can be expanded to include E2E tests.
NOTE: If you would like an in-depth and up to date introduction to various automated testing concepts and how they can be applied to Ionic/Angular applications, you might be interested in checking out Elite Ionic (Angular).
Integrating E2E Tests into a TDD Approach
I’ll walk through the general process I use when using a TDD approach that includes E2E testing, and then I’ll walk through a specific example to help demonstrate the concept. The approach still uses the basic principle of writing tests before writing code, but there’s kind of an extra layer added in there (tests within tests, if you like). The E2E test will help define the unit tests, and by implementing the code to satisfy the unit tests we will eventually satisfy the original E2E test.
1. Write an E2E test and watch it fail
The first step is to start out with writing an E2E test and watching it fail, which is the same concept we used in the TDD series for unit testing except that it’s an E2E test instead of a unit test.
2. Write unit tests and watch them fail
In order to satisfy the E2E test, we need to write some code. But with a TDD approach, we should have tests for our code before we write the code. Although technically we could say that we already have a test since we have the E2E test defined, an E2E test is a bit too generic and broad to satisfy the TDD aproach on its own. For example, we might have an E2E test defined that tests something like the user can log in. Although we technically have a test and could start writing code to allow the user to log in, we should also have additional unit tests that cover that login functionality at a more granular level.
At this stage, you should write a unit test that will move you towards the goal of satisfying the functionality described in your E2E test. In adhering to the TDD methodology, the unit test or tests that you write should initially fail.
3. Code until all unit tests are satisfied
Now you should implement the code to satisfy your unit test (or unit tests) until they all pass.
4. Check if the E2E test passes
Once all the unit tests pass, you should now go back and check your E2E test to see if it also passes. If the E2E test is not yet satisfied, you should go back to Step 2 and write another unit test. Keep repeating this process until the E2E test finally passes.
5. Go back to Step 1
Now that your E2E test is passing, you should go back to Step 1 and write a new E2E test and repeat the process. Keeping repeating this process over and over until your application is complete.
An Example of Using TDD with E2E Tests
It’s much easier to make a process like this click in your brain by seeing an actual example (as well as trying to write your own examples). I’m currently working on an app where I’m using this approach, so I figured it would be fitting to walk through one of the E2E tests I implemented with this approach.
Step 1: Write an E2E Test
One behaviour I wanted to implement in the application was for the user to be able to select a module from a list and view its lessons, so I wrote the following E2E test:
it('a user can view the lesson list for a selected module', () => {
// Select a module
let moduleToTest = selectPage.getModuleElement();
// Trigger a click to navigate to module
moduleToTest.click();
// Wait
browser.driver.sleep(1000);
// Test that there are now lessons displayed
expect(modulePage.getLessonList().count()).toBeGreaterThan(0);
});
Step 2: Write Unit Tests
The E2E test assumed the existence of some things I hadn’t created yet, like the SelectModule page, so I generated that and created the following unit test:
it('should display a list of modules once the page has loaded', fakeAsync(() => {
comp.ngOnInit();
tick();
fixture.detectChanges();
de = fixture.debugElement.query(By.css('ion-list'));
expect(de.children.length).toBeGreaterThan(2);
}));
This test assumed the existence of a data provider that had not been created yet, so I also created a couple of unit tests for that:
it('getModules should return a promise that resolves with an array', fakeAsync(() => {
moduleDataService.getModules().then((data) => {
expect(Array.isArray(data)).toBeTruthy();
});
}));
it('the data getModules provides should contain more than one module', () => {
moduleDataService.getModules().then((data) => {
expect(data.length).toBeGreaterThan(1);
});
});
Step 3: Code to Satisfy Unit Tests
With the above tests in place, I was able to implement my data provider, which then allowed me to implement the code for the SelectModule page. At this point, all the unit tests have passed.
Step 4: Check if E2E Test Passes
With the unit tests passing, I was able to go back and check if the E2E test now passes. But it didn’t because the functionality to actually click a module and go to the module page had not been implemented yet. So, I went back and wrote more unit tests to test that functionality, implemented the code, got the unit tests to pass, and tried the E2E test again. This process was repeated until the E2E test finally passed.
Step 5: Write another E2E Test
Once the previous E2E test was satisfied, I wrote my next E2E test, which was:
it('a user can select a lesson from a module and view the lesson content', () => {
let lessonToTest = modulePage.getLessonList().get(2);
// Trigger a click to navigate to module
lessonToTest.click();
// Wait
browser.driver.sleep(500);
// Check if there is content
expect(lessonPage.getContentArea().getText()).toBeTruthy();
});
and went through the whole process again.
Summary
I find that this approach works quite well for me: it adheres to the principles of the Test Driven Development approach, it provides a nice structure for building out the functionality of the application, and it will result in an application with great test coverage.