Skip to content

Sample AngularJS app in TypeScript with working httpBackend-mocked E2E tests

Notifications You must be signed in to change notification settings

bryeung/angularjs-typescript-e2e

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

AngularJS in TypeScript with working httpBackend-mocked E2E tests

What this project is

  • A sample of how one can structure a real-world large app written in AngularJS using TypeScript as the main language. I aim to have as little code as possible while giving proposed solutions to as many typical real-world large app problems as possible.
  • An example of a working AngularJS app with end-to-end tests running against a mocked $httpBackend.

What this project is not

  • a complete working app
  • a yeoman generator (but it would be nice to wrap it into one as it matures)
  • a guide to AngularJS (for that go read the docs)
  • a guide to how to write good TypeScript (for that get started with the spec and this post)
  • a guide on how to write unit tests (you could start here)
  • a guide on writing e2e tests (for some of that, work through the tutorial)

Why AngularJS?

Because it's superheroic. That aside, the angular guys did a great job on a number of fronts. The documentation is fairly decent, the feature set is close to complete, the division between controllers, directives, and services enforces a nice structure on your project. You can get rather deep without ever having to look "under the hood". Promises of values being treated no different than actual values in the data-bound views are nice.

Most importantly, angular folks made it a priority to make the framework compatible with simple vanilla JavaScript. You write functions, and are not forced to extend some heavily constraining class hierarchy. This makes it easy to integrate Angular with TypeScript.

Why TypeScript?

Have you ever gotten an error "TypeError: Object #<Object> has no method 'blah'" on the console in a browser? Chances are you've wasted many hours debugging such errors. And it could have been in large part avoided.

JS is dynamically typed and interpreted, which makes you as a developer vulnerable to runtime errors that would have been caught at compile time had you been using a statically typed compiled language. This becomes a huge pain when developing large applications.

TypeScript is JavaScript with type annotations and some other sugary features. It's compiled to readable JavaScript, and the compiler performs type checking for you, meaning you catch many errors before you spend time debugging them in the browser. You get benefits similar to those of a solid unit test suite, but at a tiny fraction of the cost. And your code gets more readable because you can easily tell what expected values for a given variable are.

The powerful interface definition syntax allows you to add type annotations to existing JS libraries, making interop between TypeScript and JS trivial yet type-safe when desired.

What's with the E2E part?

Angular comes with an e2e test runner, and provides you with a way to mock a fake http backend for your e2e tests. Sadly there are no official examples of how to get this stuff working in the context of a real application. The yeoman angular generator, while awesome, is also lacking in this department (at least at the time of writing).

So I want to put out a working example of e2e tests with a mocked backend.

Why not ToDoMVC?

I feel that ToDoMVC has come to represent a rather meaningless Hello, World! example, and tends to be implemented with the goal of writing as little code as possible so as to win in an arguably pointless category of comparison--brevity when writing trivial apps. I am trying to put together an example that better demonstrates what writing a real-world large application would feel like.

This is also not meant to be a complete working application. I am doing the minimum necessary to show off how TypeScript could be used together with AngularJS, and to make end-to-end testing work.

Okay, how do I get this running on my machine?

  1. install node
  2. clone the repo; all subsequent commands to be run in the repo root
  3. sudo npm install -g yo grunt-cli bower
  4. sudo npm install
  5. bower install --dev
  6. sudo npm install -g typescript
  7. sudo gem install sass
  8. sudo gem install compass
  9. pushd backend && sudo npm install && popd

Now that I have it, how do I see it in action?

You'll need two terminal windows. In one, run the simple backend server

cd backend

node app.js

In the other one, run

grunt server

Yes, the page with a header and four tasks is all you're expecting to see.

And you said something about testing?

Brief note about unit and e2e testing in AngularJS

Angular sets out to arm you with a powerful set of tools for testing your application. In particular, the dependency injection framework allows you to easily mock various components and thus test your code in a highly controlled fashion.

There are three flavours of testing that are of interest to me.

Unit tests with mocked, injected services

Say you're testing a controller that relies on a service. One way to test this controller is to write a fake service, just a JS function, that is used in place of the real service during the test. test/spec/controllers/TasksController.js illustrates this approach; the mocked TaskService is in test/mocks/services/TaskService.js.

Unit tests with mocked $httpBackend

Sometimes you may want to unit test a service that gets its data from a remote backend. In this case, you could mock out a fake $httpBackend to return hardcoded data against which to test. test/spec/services/TaskService.js is an example of this approach.

E2E tests with a mocked $httpBackend

This is, in a way, the holy grail of testing, in that you get to test the app that the user actually interacts with. For predictability reasons it is often desired to hardcode some of the responses that would otherwise come from a remote server, while letting the other requests straight through. test/e2e/tasks.js is an example of this approach. Configuration of what requests get mocked and which are let through happens in test/e2e/appTest.js, and the mocked data is supplied in test/mocks/data/tasks.js. All this is brought together in app/index-e2e.html.

Actually running the tests

grunt unit

will run unit tests.

grunt unit_watch

will run unit tests, and will then sit, watching for changes in all your test/**/*.js files, as well as your .js files in the app/scripts directory. Note that this does not directly include changes to .ts files. You have to have a separate grunt task running that watches your .ts files and compiles them into the tslib.js file, changes to which will then be picked up by the test runner. This can be either the grunt server task, or just the grunt watch:typescript task.

Why don't you run this task, open test/spec/controllers/TasksController.ts in your favourite editor, and change toBe(2) to toBe(3) and watch the test fail. Now fix it back to 2. Good. In another terminal window, run grunt watch:typescript, edit app/scripts/controllers/TasksController.ts and comment out the line that sets $scope.tasks in the constructor. Watch the unit test fail.

grunt e2e

will run end-to-end tests.

grunt e2e_watch

will do just what you expect it to. The interesting bit about this task is that it lets you see what's happening in the test runner. Switch to the spawned Chrome window and click the debug button. You get to see some details about the tests.

You can also run e2e test manually by running good old

grunt server

and then hitting up http://localhost:9000/test/runner.html in your browser. This could help when debugging something.

Finally, it is often useful to run the mocked version of the app directly. Perhaps you're doing backend-less development, or just want to debug e2e tests. Run grunt server and go to http://localhost:9000/index-e2e.html in your browser.

Differences from Yeoman's angular generator

This project is based on the awesome Yeoman scaffolder and the accompanying Grunt set of tasks that help with development and building the production dist. There are a few differences.

Order of compass and image minification

In the original Gruntfile that comes from generator-angular, the image minification task comes after compass. This is a problem if you like having minified images inlined into your css. I reordered the tasks. Details of the plumbing is in the comments in Gruntfile.js.

The supplied code includes a background style that uses an inlined image to demonstrate that all of this works.

Other

  • To get both unit and e2e tests running the way I like them, I tweaked both the karma configs and the testing related Grunt tasks.

Features worth noting

API typing

Note the files in app/scripts/shared/. These are used to give typing information to the request and response bodies of the HTTP APIs, which is nice because you have typed data coming in, as long as the server complies.

If you happen to write your backend in node, you can share the directory (via a git repo) with the backend, and the two will now have a tighter contract that is type checked.

TypeScript's private keyword in the constructor signature

My first impression of this keyword is that while it reduces duplication and clutter in code, it also reduces readability by having instance variables listed in two different locations in the file. So far I have been sticking with the following convention: use private in the constructor signature only on variables representing dependency injected services. That way, if you need to look up an instance variable, it's always at the top of the class, and if you need to look up a DI'ed service, it's always in the constructor signature.

The "typing chasm"

TypeScript does not yet support generics, which creates "typing chasms", places in code where typing information is lost, and it is up to the developer to recover it on the other end of the chasm.

An ng.IPromise is a typical example. We cannot yet say ng.IPromise<Task>, so inevitably when we have taskPromise.then((data) => {}), it is up to us to recover the typing information in the callback to then. So you are encouraged to write taskPromise.then((task: Task) => {}), but even if you write taskPromise.then((elephant: Elephant) => {}), the TypeScript compiler will not complain.

Public members on model objects

It tends to be helpful to make public members that would have been private due to information hiding in classic OO, for the purposes of data binding. Getter functions are the classic way to preserve information hiding yet expose the value to the users of the class, but Angular has no way of marking a return value of a function as "visited", and will thus keep evaluating the function on every digest cycle, irrespective of whether the data has changed or not, which is prohibitively wasteful.

Source maps

In Chrome dev tools' settings, check "enable source maps". If you now toss in a debugger statement somewhere in your .ts source code, when you hit that breakpoint in the browser, you'll be looking at your TypeScript code, not the generated tslib.js.

TODO and wishlist

More AngularJS samples

  • directive
  • filter

Build

  • document the build process
  • add a tiny express server to run the contents of the dist directory

Duplication in index.html and index-e2e.html

Either you end up writing ugly branching inside index.html to support both dev and prod in one file, or you end up branching them, keeping each file clean, but having duplication between the files. I chose the latter as the lesser of two evils, but am still hoping for a better solution to eventually emerge, one that removes this duplication.

That said, given that most of your js will be delivered as the compiled lib, and your css can be delivered as a top level main.css file put together with compass, changes to the index.html file that need to be compied over to index-e2e.html will hopefully be infrequent.

Test related tasks

I wish there was a neater way to define the [unit|e2e]-[|watch] set of tasks that avoids duplication. I'll revisit when I have some free cycles, though given that the Gruntfile changes far less frequently than the main codebase of the given app, I can live with the current definitions.

Transforming versus copying

In the build process, files originate in the app/ directory, get transformed (it's sometimes an identity transform) and end up in the dist/ directory. It so happens that for most files the transformer task also ends up being the mover task. Such coupling makes it impossible for me to easily turn off, say, minification, and have the non-minified versions of the files copied automatically into the dist/ directory. This is likely only a problem for when you're actually tweaking the build process, which few people will hopefully have to do.

Origins

This setup is the result of tweaking the configs and the tools as part of working on two distinct apps. Neither of them are in production as of this writing, and the dev process is still evolving, so more changes should be expected.

Anticipated questions

  • Why are the tests in JavaScript, and not in TypeScript
    • Yeah, silly, I know. I started out with the tests that came with the yeoman scaffold, and haven't bothered to transition them to TypeScript yet. I suppose I wasn't convinced that there's much to be gained from the transition, other than uniformity of language.

Conclusion and credits

I've been frustrated with the state of web development for years. I feel that now we are, for perhaps the first time ever, well-equipped for building large, complex yet robust applications that work in the browser. This is possible thanks to the fine folks who created:

and many others.

About

Sample AngularJS app in TypeScript with working httpBackend-mocked E2E tests

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Languages

  • JavaScript 88.9%
  • TypeScript 11.1%