Search code examples
angularjskarma-jasmineuglifyjs2

Angular Mocks with minification


Mission:

I have a legacy, un-minified Angular 1 app with thousands of tests, all passing. I introduced minification and mangling to the code. If I keep the libs and app code separate, the tests continue to work. However, I can't mangle the app code because doing so will break the contract back to the libs files.

I want to be able to mangle the app code and keep the unit tests working.

Current State:

I've created two paths to circumvent the problem, but I'm not satisfied because the unit tests aren't really hitting the code which will be in production... they're hitting a middle ground created to avoid a mangling problem.

Path 1

One path outputs a libs.min.js and an app.min.js. Inside of libs.min.js are things like Angular, Underscore, and so on - using their provided minified versions. Essentially libs.min.js is just a bunch of already minified files concatenated together. app.min.js contains our application code, minified but not mangled.

Path 2

The second path creates a combined.min.js file. This is what goes to production. Inside of this file, in order, are Angular, Underscore and so on, followed by our application code. All of these are concatenated in to this file unminified (including the libs) and then mangled/minified together so that we get a single source map which can reference our app code as well as the library code. This works in the browser, but the unit tests break because combined.min.js does not include the required testing-only library angular-mocks.

Problem:

Testing against path one works because Karma loads these files. This is fine, but it's not the actual code that will be going to production so I don't count this as a win.

  1. libs.min.js
  2. angular-mocks (unminified - and can bind in to Angular because we're using the provided minified version)
  3. app.min.js (minified but not mangled, because mangling breaks the contract back to libs.min.js)
  4. *.spec.js

Testing path two does not work because we load things in the following order:

  1. combined.min.js
  2. angular-mocks (unminified)
  3. *.spec.js

Kaboom. Our app code has already hooked up its bindings so angular-mocks is too late. I don't see any way to get around this besides loading libs and app in to separate files, like in path one.

Some of the tests pass, but those trying to reference anything we're relying on angular-mocks for fail.

I know I can provide an input source map to app.min.js so that I can minify/mangle the app code it separately (but consistently against libs.min.js), but I'm not sure that's the best route. I'd like to keep things in one big file but if there's no other way, it looks like the library I'm using to mangle/minify (UglifyJS) supports input source maps...

Oh. Also, we're not using Grunt or Gulp. Just straight NPM to do all of this. But if there's a grunt or gulp library that somehow solves this, I can NPM-ify it.


Solution

  • I figured this out. The original question isn't quite correct in its diagnosis of what was wrong. I doubt anyone else will stumble upon this type of problem, but I'll leave it here just in case.

    Problem 1:

    By minifying the libs and apps together, we have required some libs to work in strict mode which originally weren't in strict mode. EIther they're not in a self executing function, or there's a global "use strict" leaked from some other library... there are a lot of reasons why this could happen.

    In my case, the offender was Restangular. Version 1.4 will not work with strict mode, because there is an implicit definition of 'data'. I'm sure this is an oversight in an old version, but nevertheless it will cause errors in Phantom about "no such property 'data'".

    This is an easy thing to fix. Most likely, just upgrade to a newer version. Upgrading to a newer version is not permissible for this application, so for now I just manually added 'var data' to trick the definition.

    Problem 2:

    Our Jasmine tests begin failing. They're executing and consuming the minified code though. An example of what happened when running against the minified code:

    var result,
        someObject = {foo: "bar"};
    
    httpBackend.whenGET("/something").respond(someObject);
    
    somethingService.then(function(response) {
       result = response;
    });
    
    httpBackend.flush();
    
    expect(result).toBe(someObject);  // fail.  {foo: "bar"} is not {route: "something", getRestangularUrl: function...}
    

    Normally this works, because ngMocks bypasses Restangular's response wrapper; the object you respond with will be what is passed to the resolved promise, without the Restangular meta data added on. So {foo: "bar"} remains exactly {foo: "bar"}.

    But, since we loaded ngMocks after our app code (see 'path 2' in the question), Restangular's response is no longer bypassed. Therefore, Restangular extends your intended response with all of the extra meta data.

    So, our {foo: "bar"} becomes an entire Restangular response which contains {foo: "bar"}, but which isn't only {foo: "bar"}.

    Resolution: don't test the entire object. Check for the specific key and that specific key's value.

    var result;
    
    httpBackend.whenGET("/something").respond({foo: "bar"});
    
    somethingService.then(function(response) {
       result = response;
    });
    
    httpBackend.flush();
    
    expect(result.foo).toEqual("bar");  // pass
    

    I'd argue that testing in this manner is better since it's a more accurate representation of what's actually going to happen once ngMocks is gone anyway.