Check out our Angular Book Series.

How to spy on a Property with Karma?

When writing unit tests, SpyOn, is used to watch and manipulate a method call, potentially returning your own data. This is used to easily mock external component calls so that your unit test can focus on the class in question without worrying about complications or side affects from external dependencies. I found myself in a situation where I had a class and that class had a read only get() property. I was creating my own Angular service wrapper for the Dragula drag and drop library. I was exposing some drop and remove listeners get methods.

I wanted to control the results returned from that get method. This was not a situation where I could set the underlying value because there was no set method. But, spyOn would not work because a get() method is viewed as a property, not a method. The class code was conceptually like this:


export class MyClass {

constructor(private dragula: DragulaService){
}

get dropListener(){
return this.dragula.drop('myDragulaGroup');
}
}

My code was using Angular, so the DragulaService was injected into this component, and the dropListener was exposed via a get.

For the purposes of unit testing, I want complete control over that drop listener Observable. Thankfully, spyOnProperties would give me that control.

The unit test would be a bit like this:


it("Should spy on Observable", () =>
{
const myClassInstance = new MyClass();

let dropObserver: Observable<any>;
const dropObservable: Observable<any> = Observable.create( ( ObserverArg: Observer<any>) => {
dropObserver= ObserverArg;
}

mySpy = spyOnProperty(myClassInstance, 'dropListener', 'get').and.returnValue(dropObservable);

I'm gonna take a quick break to explain spyOnProperty. It takes three parameters:

  • Class: The class instance that contains the property you want to watch.
  • Property: The name of the property being watched.
  • Accessor: The Access Type of the method being watched, in this case get.

Note that we are storing the results of the spyOnProperty in the mySpy variable. This is optional when using spyOn, but for spyOnProperty it is required. The unit test could would continue:


const val = myClassInstance.dropListener;

All that line does is access the dropListener property inside the class, which should trigger the spy. Now we can test against the access:


expect(mySpy ).toHaveBeenCalled();
});

In a more advanced unit test, we could subscribe to the dropListener inside the test, and use dropObserver.next() to trigger the result handler in an async block.

How do I prevent a Browser from opening up when I run Angular unit tests?

When you run unit tests using the Angular CLI's ng test command you'll often see a browser open up. Sometimes this is useful if you want to debug the unit tests in the browser, however since I have moved the bulk of my unit test debugging to IntelliJ, this is just a minor annoyance.

You can shut it off. Open up the Karma.conf.js file in the src folder of your Angular CLI project. Find this line:


browsers: ['Chrome'],

This tells Karma to open Chrome to run the tests. But, there is another option:


browsers: ['ChromeHeadless'],

Rerun your tests and you'll find that no browser opens up.

I love it when things are easy.

Why won't my Observable Trigger in a Unit Test?

I've been writing some tests as part of a project I'm working on, and for some reason I could not get a mocked Observable to resolve. This will explain the mistake I made and how to fix it.

The Setup

This component used a resolver to load data before the component loaded. To access a resolver's data, you inject the activated route into the constructor, like this:


constructor(private route:ActivatedRoute){}

In the ngOnInit(), you'd subscribe to the data property:


ngOnInit(){
this.route.data.subscribe((data: {myValue}){
this.myValue = data.myValue
}
);
}

This saves your MyValue data from the resolver into the local component instances myValue.

Writing The Tests

First, I mocked the ActivatedRoute:


class ActivatedRouteMock {
data: Observable<Data>;
}

Then, I created an instance of it:


const activatedRoute = new ActivatedRoutMock();

As part of the TestBed's providers:


{ provide: ActivatedRoute, useValue: activatedRoute}

The first test I wrote was to make sure the subscribe was actively working.

First, I created the observer and component:


const observer;
beforeEach(() =>
{
activatedRoute.data = Observable.create (observer) => {
observer = observer;
}
spyOn(activatedRoute.data, 'subscribe');
component = TestBed.createComponent(myComp).componentInstance;
});

Then, I ran the test:


it('should subscribe to activated route', () =>
{
expect(activatedRoute.data.subscribe).toHaveBeenCalled();
});

This worked great. Now, I wanted to test to make sure that other methods inside the component's result function worked. To do that I'll have manually trigger the observer:


it('should resolve route data and save myValue'. ()=>
}
const myValue = "something";
const results = {};
results['myValue'] = myValue;
observer.next(results);
observer.component();
expect(component.myValue).toBe(myValue);
});

This should work, right? Nope, it failed ever time. The observer was always null. I was scratching my head and adding a lot of breakpoints and outputs trying to figure out what I had done wrong so far.

What is wrong

The function to create the observable is never run until the observable is subscribed to. Clearly we are subscribing to it in the component code:


this.route.data.subscribe((data: {myValue}){
this.myValue = data.myValue
}

So, why doesn't it run? Think about it first, because spoilers are below.

The spy is getting in the way:


spyOn(activatedRoute.data, 'subscribe');

Because of the spy in place, the subscribe() function is intercepted, and as such never run, which never runs the function in the Observable's create function, which never saves the Observer. The solution was simple once I figured out what was going on:


spyOn(activatedRoute.data, 'subscribe').and.callThrough();

Tell the spy to intercept and watch, but still allow the call to go through.

Everything started working wonderfully. :-)

How do I test Observable.timer?

This was trickier than I thought, which is why I'm writing a blog post about it. I'm writing some code which uses an FakeAsync Zone nor RxJS TestScheduler.

The timer() call returns an Observable, so I decided to create one myself which gave me complete control.


let timerObserver :Observer<any>;
beforeEach(() =>
{
spyOn(Observable, 'timer').and.returnValue(Observable.create(
(observer =>{
timerObserver = observer;
})
));
});

I created an Observer object. This is the internal logic that makes the Observable resolve itself. I used spyOn() inside a beforeEach() to have the unit testing framework return my own Observable and ignore the library code. I save the observer for later usage.

Now, when I'm testing I can resolve the timer immediately:


it('Some Test',()=>
{
// do stuff if needed

// trigger the fake timer using the Observer reference
timerObserver.next('');
timerObserver.complete();

//
expect(somethingToHappenAfterTimerCompletes).toHaveBeenCalled();
});

I spent more time banging my head on this than I thought I would, and I hope this helps you.

Running Unit Tests from IntelliJ

I put together this screencast about running Unit Tests using IntelliJ.

Unhandled rejection Error: Not running at Server.close (net.js:1236:11) with Karma and Jasmine

I'm writing some addendum to my LearnWith AngularJS training series. One of those will detail how to unit test an AngularJS web application. I'm setting something up with Gulp, Karma, and Jasmine.

Trying to get this running gave me a full day of this error:


Unhandled rejection Error: Not running
at Server.close (net.js:1236:11)
at C:\Users\jhouser\Documents\career\clients\ActiveClients\LearnWith\Development\www_codearchive\Scripts_Test\node_modules\karma\lib\server.js:388:17
at tryCatcher (C:\Users\jhouser\Documents\career\clients\ActiveClients\LearnWith\Development\www_codearchive\Scripts_Test\node_modules\karma\node_modules\bluebird\js\release\util.js:16:23)
at Promise._settlePromiseFromHandler (C:\Users\jhouser\Documents\career\clients\ActiveClients\LearnWith\Development\www_codearchive\Scripts_Test\node_modules\karma\node_modules\bluebird\js\release\promise.js:510:31)
at Promise._settlePromise (C:\Users\jhouser\Documents\career\clients\ActiveClients\LearnWith\Development\www_codearchive\Scripts_Test\node_modules\karma\node_modules\bluebird\js\release\promise.js:567:18)
at Promise._settlePromise0 (C:\Users\jhouser\Documents\career\clients\ActiveClients\LearnWith\Development\www_codearchive\Scripts_Test\node_modules\karma\node_modules\bluebird\js\release\promise.js:612:10)
at Promise._settlePromises (C:\Users\jhouser\Documents\career\clients\ActiveClients\LearnWith\Development\www_codearchive\Scripts_Test\node_modules\karma\node_modules\bluebird\js\release\promise.js:691:18)
at Async._drainQueue (C:\Users\jhouser\Documents\career\clients\ActiveClients\LearnWith\Development\www_codearchive\Scripts_Test\node_modules\karma\node_modules\bluebird\js\release\async.js:138:16)
at Async._drainQueues (C:\Users\jhouser\Documents\career\clients\ActiveClients\LearnWith\Development\www_codearchive\Scripts_Test\node_modules\karma\node_modules\bluebird\js\release\async.js:148:10)
at Async.drainQueues (C:\Users\jhouser\Documents\career\clients\ActiveClients\LearnWith\Development\www_codearchive\Scripts_Test\node_modules\karma\node_modules\bluebird\js\release\async.js:17:14)
at process._tickCallback (node.js:419:13)

There is a lot of Google hits for this error; but none actually relate to my issue, so I hope this helps someone.

After much head pounding and experimentation; I finally deciphered the cause. When creating the Karma config file, make sure that your files are put in proper dependency order. Simply put; I was listing the AngularJS library at the end of the file list instead of the beginning, so the code didn't know how to create angular modules, services, or other elements, because that Angular library wasn't defined yet.

Changing the order of dependencies solved the issue and I can run unit tests as expected.

I hope that helps someone.

Sign up for DotComIt's Monthly Technical Newsletter

All Content Copyright 2005, 2006, 2007, 2008, 2009 Jeffry Houser. May not be reused without permission
BlogCFC was created by Raymond Camden. This blog is running version 5.9.2.002.