Saturday, March 19, 2016

And I am so done with this, too...

Before I get started, let me just say that I like a lot of things about AngularJS.

The basic approach of using directives in the HTML is very appealing and seems natural; filters are easy and intuitive; custom directives provide great options to adding function with little fanfare; data models fit right in to JavaScript; and the ease with which data flows between the view and the data model is pretty much seamless. Google being behind it in a big way is also a major plus.

But what really excited me about AngularJS was that it had "Testability Built-in" [that's a quote from the AngularJS home page]. I'm a believer in TDD/BDD so that was a big selling point for me.

However, my experience trying to get TDD actually working led me to a different conclusion. Here are some of my experiences.

1. Creating a logger requires what? Logging is simple... right? In Ruby it's just:
logger = Logger.new("my_log.log")  # or STDOUT if you want to make it really simple
logger.error "This is my error"
 
In the Javascript world, it's even simpler:
console.log("This is my error")
 
Sorry, Angular... you make it really ridiculous:
describe('basic test', function(){
    var log;
    beforeEach(inject(function(_$log_){
        log = _$log_;
    }));

    it('should just work', function(){
        log.info('it worked!');
        expect(log.info.logs).toContain(['it worked!']);
    });
});
And notice the crazy little "trick" where you say _$log_ to get this to work. Yeah, it's "documented" but buried away and not exactly readily available.

2. "injector already created. can not register a module"
Hmmm... Angular knows that we want to register a module, they know that the injector has already been created, so why can't they Just Fix It? They do all kinds of other magic behind the scenes; this sounds like an easy one. If they can't Just Fix It, then how about a meaningful error message and documentation explaining why that really happens. This is one of those cases where you have to know the innards of Angular to be able to figure things out.
See SO: http://stackoverflow.com/questions/24900067/injector-already-created-can-not-register-a-module

3. How many different ways are there to 'inject' stuff into your test.. (I sure can't count 'em). In fact, why do you even have to play that game at all? If you look around, you'll find countless ways to inject things...

from: http://andyshora.com/unit-testing-best-practices-angularjs.html
describe("Unit Testing Examples", function() {

  beforeEach(angular.mock.module('App'));

  it('should have a LoginCtrl controller', function() {
    expect(App.LoginCtrl).toBeDefined();
  });

  it('should have a working LoginService service', inject(['LoginService',
    function(LoginService) {
      expect(LoginService.isValidEmail).not.to.equal(null);

      // test cases - testing for success
      var validEmails = [
        'test@test.com',
        'test@test.co.uk',
        'test734ltylytkliytkryety9ef@jb-fe.com'
      ];

      // test cases - testing for failure
      var invalidEmails = [
        'test@testcom',
        'test@ test.co.uk',
        'ghgf@fe.com.co.',
        'tes@t@test.com',
        ''
      ];

      // you can loop through arrays of test cases like this
      for (var i in validEmails) {
        var valid = LoginService.isValidEmail(validEmails[i]);
        expect(valid).toBeTruthy();
      }
      for (var i in invalidEmails) {
        var valid = LoginService.isValidEmail(invalidEmails[i]);
        expect(valid).toBeFalsy();
      }

    }])
  );
});

from: http://stackoverflow.com/questions/12758157/how-to-inject-dynamically-dependence-in-a-controller
var algoController = function($scope, $injector) {
    $scope.base64 = $injector.get('base64');
};
But then there's the 'dynamic' alternative
var algoController = function($scope, base64) {
    $scope.base64 = base64;
};

Or how about this, from: https://www.airpair.com/angularjs/posts/testing-angular-with-karma
function MyController ($scope) {
  $scope.property = 'value';
}
MyController.$inject = ['$scope'];

But then there's this (which won't work with minified code)
function MyController ($scope) {
  $scope.property = 'value';
}
angular.module('myApp', [])
  .controller('MyController', [
    '$scope',
  myController
]);

I'm sure that all these methods work just fine for the folks who documented them (nothing against them; I certainly respect what they can do). But trying to figure out a sane way to do things when you're getting started is... interesting.

On http://stackoverflow.com/questions/31977313/tdd-with-angularjs-and-protractor.
A question I asked with no response...

I could go on and on with the attempts I made to get a reliable, stable approach to injecting code for testing. In the end, I gave up. I'm sure that Angular supporters will scoff and say: "Hey! It's really easy... just do this".

4. So I asked friends who were working with Angular for some help, I got comments commiserating with me about how it's all not very clear, confusing, overly-complicated, difficult, etc. Some folks even pointed to the fact that you have to know a lot about how Angular works internally before you can easily create real tests.

However, the kicker for me was when I asked a member of the Angular core team about testing and his response was a shrug, a pained expression and the comment: "Yeah; we need to work on that."

That's when I said: "Yeah; I need to move on to something else..."