Now we're figured out the testing technology stack and what we need to decide before we can write end-to-end tests it's time to actually get down to writing some test code. Let's look at what parts go towards making a complete test. To understand that we need to see what a test looks like.
Don't worry if that makes no sense to you yet. If you're not familiar with end-to-end testing then it won't. If that is the case though, read through the code in this test and see if you can figure it out. It should be quite straightforward. Test code is supposed to be very readable.
The very first line of our example test is a describe() call with a title and a callback function. Tests are organised in to suites, groups and then individual tests. A suite comprises of any number of groups and a group contains any number of tests. In general it's sensible to organise your tests this way in order to be able to run small numbers of tests in order to quickly check code is valid.
In most test runners a suite of tests is defined by putting them all in the same file. You can think of the example above as an entire test file. It's quite short, but there's only one test assertion in it. A test file could have a lot more tests. You might have a suite of tests to check that a component is valid, so you'd locate them all in "component.tests.js". A test suite can lots of tests, so it's good practise to organise them in to groups; a group is defined by the describe() call. By using a good description of what you're trying to test as the title passed in to the describe() you can produce great reports.
;
You can nest describe() functions, so you can organise your tests more deeply.
;
Once you've set up a basic group it's time to make it run some tests, but before we do that let's quickly look at running a function before and after each test. This is important because you'll need to get the testing environment to a position where you use the functionality you need to check first.
Every test in a group should be using the same basic initial settings. If you don't get your environment to a known good state before the test runs then you'll be testing it in an unknown state, which means things might not work properly. Or worse, they might work because the state of the application just happens to be right. Testing is all about knowing what's supposed to be happening and checking that it actually is.
For example, imagine you have a page in your app that only works when the user is logged in. If you test it in an unknown state then some of the time it will fail and some of the time it will pass, depending on whether or not the browser is logged in or not. That's not useful.
Test runners afford you the possibility to run a function before every test, and a different function afterwards. This means you can do things that get the browser to a point where it needs to be before the test runs, then run the test, and then reset the browser to a state where the next test can start from.
;
What this means is that before every test the test runner will load http://testdomain.dev/, then click on the 'login' element after the page has loaded. In this simple example that's enough to log in. Then each test runs, and afterwards the test runner clicks on the 'logout' element. By doing this we can know for certain that the browser is in the right state.
The next part of the test is the most relevant bit - the part that actually tests something. There are a lot of different tests - you can check if something is visible, if it's clickable, if it has a specific attribute or CSS style, if its content is exactly like a string, or partially like a string, or matches a regular expression. You can even do things like taking screenshots to push to pdiff as we saw in the Visual Testing section. Practically anything that you can think of already exists as a testing function, and for anything that isn't there you can write new functions to test with too. Remember, tests are code, and that's really powerful.
Each test is made up of a list of expectations, and they're all neatly bundled together inside of an it() function. it() is so-called to be semantic - 'it', which refers to the application in the browser, should have a descriptive title that says exactly what 'it' should look like at the moment the test runs.
it() takes two parameters: a title for the expected condition, and a callback that contains the expects() calls that check things and any control code that makes the browser do stuff. This is where the majority of your test code will live.
An expect() call is usually the beginning of a chain of methods. A locator function (something that gets a part of the page to test) is passed in, and then an assertion is chained after. Putting all this together we get a completed test group.
Here we have a test group for 'Test the Web app' defined in the describe() function that contains a beforeEach() callback to go to a test website and log in, then check that the username is displayed in the navigation. The expect() call finds an element with an id attribute of 'user' and returns it's text content, and the assertion toBe() checks that the value is exactly "chris@ooer.com".
There are different element locator functions that we could have used to find the element in the page. What's actually available depends on your testing framework, but most make finding things by tag, ID, CSS, and XPath available. Protractor adds finding things by model and binding too, which makes sense in an AngularJS context by not so much for other web applications.
The element() object also includes finding more than one element in a page through the .all() method. If our example returned a list of elements we could then do things like testing the number of items, which can be a useful way to determine if things are being added to a page through JavaScript.
When you're testing your web application it's important to test the edges of the code. What this means is that you should test the interfaces where pieces of code talk to each other rather than testing the internals of the code. If you make your tests deeply integrated with the internals then you won't be able to change any of it without the tests breaking. Instead you should aim to write test code that doesn't rely on the specifics of how the code works.
One way to do that is to remove the code that interacts with a page from your test code completely. The way to do this is by using pageobjects. A pageobject takes the parts of the test that find elements within a page and abstracts them away in to a separate block of code. This might seem redundant. You're just taking the code and moving it from one place to another; if the application changes then the pageobject will break instead of the test code, so the end result is essentially the same. The reason why pageobjects are a good idea is because it means you can have completely different implementations of a page, each with completely different pageobject code, but with the exact same tests. It might seem like that's unlikely, but it means you can completely refactor a page and it's pageobject interface code in a feature branch without changing the tests at all. That's a huge win. Pageobjects also cut down on code replication if your tests interact with the same elements on the page several times.
A pageobject uses the same locator code as a test would, but abstracts it away in to a separate code block. The pageobject code block can then be imported into any test suite that needs to interact with the page it refers to.
var { browser;}; loginpageprototype = Object; moduleexports = loginpage;
The loginpage object can then be instantiated in any tests that need to access the login page and login button.
var loginpage = ;
By working this way it should be pretty obvious that, for example, we can refactor the login page to change the button's ID to something else and we'd only need to change the pageobject code. Any test that uses the login button would then work again. We wouldn't need to change our tests at all.
The next thing to learn is how to actually find things in the page to test. This is where the element() object comes in to play.