Angular Functional Testing: The middle ground.
TDD might be dead, but BDD is still kicking for Angular apps, if you do things right that is. I mean functional tests. They aren't built into angular, but they're a really a great way to do things, in our humble Hint Health opinion. They're just the right mix of unit and E2E tests. Let me give some context, and then let's dive into how you can use fun, resilient functional tests with your angular app today.
The context: Angular comes with two main ways to test. Unit, and E2E (End-to-End) tests, but neither was quite what we wanted at Hint Health. Unit tests are meant for testing controllers, filters, etc. in isolation. These have their place, but they're brittle and fail in the face of even slight refactors (at Hint, we mainly use these for directives and filters, which don't seem to change much after they're working). E2E tests are meant to be like a real user clicking and moving around your site, hitting the real API, with real data, etc. This is rad, and more resilient to refactors, but it's slow, more heavy-weight, and requires the real API and database to be up and ready for whatever you're testing (which isn't always the case when you're in the middle of developing new front-end features).* Is there no middle ground?
The solution: What if we could test use cases directly, just like a user would, but without all the baggage of a selenium server and a real API? Something like...
visit('/some_page') someButton.click() httpBackend.expect(route, params).respond('result') expect(someViewValue).toBe('result')
That would be sweet. That test wouldn't care if you use the built-in router, or UI-router. It wouldn't care what your controller is called, or whether you manipulate view data with a filter or a directive. All it would care about is the contract with your API and the end result to your user. It would also be fast, and and allow for mocking where you need it. This would be awesome because you could refactor with confidence, run tests quickly, but most importantly, drive your front-end development with real test cases, from a user's perspective. We can do it. It's actually pretty easy. Let's make it happen.
KarmaĀ (You need this for angular unit tests anyway)
JasmineĀ (Recommended to go with Karma)
Jasmine-jquery (Extremely helpful test matchers for this kind of thing)
Rosie (for front-end factories. Not 100% necessary, but data mocking will get unwieldy real fast without a tool like this.)
Compile a skeleton view that only contains your app and your view directive
Start pretending you're a user and expecting certain things to happen.
https://gist.github.com/bwest87/89404aad37ac2f517ca9
The critical lines are as follows....
$view=angular.element("<ng-app='my-app'><ui-view></ui-view></ng-app>")
$scope = $rootScope.$new()
It's only three lines of code, but here's what's going on. The view directive in the middle of the html-string watches the url we're currently on, and since it's wrapped by your app, it will load the appropriate template and controller, just as it would when someone visits that link normally. If you use ui-router, you'd use 'ui-view' here. But if you use angular's built-in router, you would use 'ng-view'. The next line creates a new rootScope, and finally, the $compile step binds that scope to the view, and this is what will connect up the controllers and views that will get called. Pretty awesome. And the real beauty here is how little it cares about implementation. Just to go the URL, and shit should work.
You'll need two kinds of mocking.
Routes: Use $httpBackend for this. When your controllers ask for things, this is where you'll set up the fake responses. This is also how you can check that a certain backend route was hit, and that it was hit with certain paramaters.
Data: We use Rosie for this. It's a way to easily create test data on the front end. It's inspired by (though not as nice as) FactoryGirl, which is the go-to test data maker for Rails. We stow away all our factories in a separate 'dataMocks.js' file, and then just include that in our Karma config, so they're globally available to any test.
Just pretend you're a user.
You should not access the scope, nor should you care. If the thing you want to test doesn't either show up on the front end, or end up hitting a back-end route, then you shouldn't care. Or that test belongs somewhere else.
The whole point is to be agnostic of the implementation details of the view, controller, filters, directives, etc. If you refactor some controller functionality into a service, the tests should still pass. If you change the name of your filter, the tests should still pass.
Use the most resilient selectors you can. That means jQuery selectors that are less prone to change. As in, something like āa:contains('Sign Up')ā rather than like ā[ng-submit='signUp()']ā. You might change that controller action name, or end up doing an ng-click instead of ng-submit. And sure you may change the text from 'Sign Up' to 'Join Now', but at least that version is more user-centric and knows nothing about your implementation.
Cache buttons, or interesting divs in a beforeEach. (using jquery-jasmine) (but see the caveat below...)
Use jQuery's 'trigger' method for all actions. You might think you can do something like... āinputBox.val('someValue'), but this won't do what you think it will. What you want is... āinputBox.val('someValue').trigger('input'). This will have the side effect of triggering an $apply(), and so everything will work as expected. Same with clicks, submits, etc.
Use $scope.$apply() when jquery triggers don't make sense.
You do not have access to CSS properties when testing! Most of the time, this isn't a big deal, but occassionally it can bite you. For example, the jasmine-jquery matcher āisVisibleā relies on checking the display property (which isn't available). That's the only one that's come up for us, but good to know. You can instead check if the element has the css class āng-hideā, which is applied to any element that was hidden through an ng-hide/show.
Reload view containers if you need to check them! Sometimes you'll click a button, or input something, and expect a different value to appear in the view after the digest loop has run. The updated values won't be there if you cached the jquery selector. You'll have to go get the element again in the pertinent test.
Select boxes won't respond to .trigger('input'). You'll have to use .trigger('change').
This set up has really let us write more resilient front-ends that we feel confident releasing. When our product tester finds a bug, we can just write a test for it and make it pass. Most importantly though, it lets us think from the user's perspective as we develop. Big win!
Happy Testing! Leave comments or questions if you have thoughts!
*Yes, you can mock things with E2E tests, but according to comments from Ari Lerner, author of ng-book, mocking with E2E kind of defeats the purpose.