Showing posts with label Jasmine. Show all posts
Showing posts with label Jasmine. Show all posts

Monday, May 23, 2022

No provider for ControlContainer!

As you can probably deduce from the title, this post is all about an annoying error in Angular that reads "No provider for ControlContainer!". If you're just getting this error in general, the first thing you should do is make sure you've imported the FormsModule and ReactiveFormsModule (yes, both of them). That will probably clear it up for you. If you've done that and you're still getting this error, specifically while running your unit tests, I have a solution for you. I'm nearly 100% certain I didn't come up with this on my own, but since I'm not sure where I originally found it I can't link you over to it. Sorry for that.

First off, let me show you the code that triggered this issue when I started testing. Like a good programmer following DRY (Don't Repeat Yourself) I will often componentize even small things that are reused and require some bit of setup or configuration. For instance, when creating forms that require masked input fields (like phone number or credit card) I'll create a component that allows me to quickly and easily drop that into my form and just specify which mask to use. The way I do that requires me to inject ControlContainer directly into the component. Here's some example code:

import { Component, Input, OnInit } from '@angular/core';
import { ControlContainer, FormControl, FormGroup } from '@angular/forms';

@Component({
  selector: 'app-masked-input'
  templateUrl: './masked-input.html'
})
export class MaskedInputComponent implements OnInit {
  @Input() controlName: string;
  @Input() label: string;
  @Input() mask: any;

  constructor(public controlContainer: ControlContainer) { }

  public ngOnInit(): void {
    // Set our form property to the parent control
    // (i.e. FormGroup) that was passed to us, so that our
    // view can data bind to it
    this.form = this.controlContainer.control as FormGroup;
    this.control = this.form.get(this.controlName) as FormControl;
  }
}

That's the relevant part to this error. If we try to run unit tests around that code we'll get the error "No provider for ControlContainer!" even if we import FormsModule and ReactiveFormsModule in our test file. To fix that error, we have to manually create a FormGroupDirective in our test file and change the provider for ControlContainer to use that FormGroupDirective. Here are the relevant bits of code:

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ControlContainer, FormControl, FormGroup, FormGroupDirective, FormsModule, ReactiveFormsModule } from '@angular/forms';
...snip...
describe('MaskedInputComponent'), () => {
  let component: MaskedInputComponent;
  let fixture: ComponentFixture<MaskedInputComponent>;
  const formGroup: FormGroup = new FormGroup({
    dynamicControlName: new FormControl('')
  });
  const formGroupDirective: FormGroupDirective = new FormGroupDirective([], []);
  formGroupDirective.form = formGroup;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [ MaskedInputComponent ],
      imports: [ FormsModule, ReactiveFormsModule ],
      providers: [ { provide: ControlContainer, useValue: formGroupDirective } ],
    })
    compileComponents();
  });

  beforeEach(() => {
    fixture = TestBed.createComponent(MaskedInputComponent);
      component = fixture.componentInstance;
      component.controlName = 'dynamicControlName';
      fixture.detectChanges();
    })
    compileComponents();
  });
});

That's it! This will alleviate the "No provider for ControlContainer!" error in your test run. It's simple enough once you know what to do, but it was a pain figuring it out. Hope this helps.

Wednesday, January 22, 2020

Debouncing Jasmine

I haven't been writing many unit tests lately, especially not in Jasmine for Angular. That led to a problem today that sucked up a few hours of my time and it really shouldn't have. I feel like at this point I've tested pretty much every "normal" thing I can so when I get stumped it really annoys me.

I have a typeahead control that uses debounceTime (from rxjs) to wait 250 milliseconds before firing off the request to the server. We do that to avoid banging against the server while the user is still typing something. In conjunction with debounceTime, we use switchMap to cancel the previous request and make sure the results we get back match what was actually searched for. When I tried to test this setup I was kept seeing that my spy had not been called and I could not figure out what was going on.

I finally Googled the right combination of terms and stumbled onto this answer on Stack Overflow. The gist of what's happening is that the debounceTime wasn't "passing" so the call to my service was never made. Here's the weird part, though: I added a bunch of logging and I could see that the service was receiving the call. That's why it took me so long to figure out what was going on. From my perspective it looked like the real service was being called instead of the spy I placed on the real service. That naturally sent me searching for known issues in Jasmine when the problem was with the design of my test.

OK, with all of that said, here's the component I was testing (or a close proximity of it anyway):
   1:  export class TypeaheadComponent implements OnInit {
   2:    public searchTermSubject = new Subject<string>();
   3:  
   4:    constructor(private searchService: SearchService) { }
   5:  
   6:    ngOnInit() {
   7:      this.searchTermSubject.pipe(
   8:        debounceTime(250)
   9:      )
  10:      .pipe(
  11:        switchMap(term => {
  12:          // check the term and do other stuff here before calling the server
  13:          return this.searchService.search(term);
  14:        })
  15:      ).subscribe((searchResults) => {
  16:        // do something with the results
  17:      });
  18:    }
  19:  
  20:    triggerSearch(term: string): void {
  21:      this.searchTermSubject.next(this.searchTerm);
  22:    }
  23:  }
  24:  

It looks pretty straightforward to me! Before I show the test that was failing, here's the overall test setup. I think it's important to include this so because I hate when I find an answer and something doesn't work quite right because whoever wrote it didn't show their import/using statements.
   1:  import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
   2:  import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 
   3:  
   4:  import { of } from 'rxjs';
   5:  
   6:  import { SearchService, MockSearchService } from '../providers'; 
   7:  
   8:  import { TypeaheadComponent } from './typeahead.component'; 
   9:  
  10:  describe('TypeaheadComponent', () =>
  11:    let component: TypeaheadComponent;
  12:    let fixture: ComponentFixture<TypeaheadComponent>;
  13:    let searchService: SearchService;
  14:  
  15:    beforeEach(async(() => {
  16:      TestBed.configureTestingModule({
  17:        imports: [FormsModule, ReactiveFormsModule],
  18:        declarations: [TypeaheadComponent],
  19:        providers: [
  20:          {
  21:            provide: SearchService,
  22:            useClass: MockSearchService
  23:          }
  24:        ]
  25:      }).compileComponents();
  26:  
  27:      searchService = TestBed.get(SearchService);
  28:  
  29:      spyOn(searchService, 'search').and.callFake(() => of({}));
  30:    }));
  31:  
  32:    beforeEach(() => {
  33:      fixture = TestBed.createComponent(TypeaheadComponent);
  34:      component = fixture.componentInstance;
  35:      fixture.detectChanges();
  36:    });
  37:  
  38:    // tests go here
  39:  
  40:  });
  41:  

Now that we've established that, here's the test I wrote to make sure the service function (search) was called:
   1:    describe('search', () => {
   2:      it('should search', () => {
   3:        component.triggerSearch('term');
   4:  
   5:        expect(searchService.search).toHaveBeenCalled();
   6:        expect(searchService.search).toHaveBeenCalledWith('term');
   7:      });
   8:    });
   9:  

Unfortunately, that's where the test was failing with an error message indicating that the spy should have been called, but it wasn't. Even looking at it now it still looks good to me. As I mentioned above, I added a bunch of logging to see what was going on and all the right spots were being hit, but my spy wasn't being called. It turns out I had to make a very small change that would "tick" the timer, causing the debounceTime function to allow everything to happen. Here's the updated test:
   1:    describe('search', () => {
   2:      it('should search', fakeAsync(() => {
   3:        component.triggerSearch('term');
   4:        tick(500);
   5:  
   6:        expect(searchService.search).toHaveBeenCalled();
   7:        expect(searchService.search).toHaveBeenCalledWith('term');
   8:      }));
   9:    });
  10:  

That was it. Wrap the test in the fakeAsync function and invoke a tick(500) after invoking the triggerSearch function. Now everything works as I expected.

Like I said at the beginning, my problem was that I assumed from the error message that something was wrong with Jasmine or the injection process in Angular. That's partly why it took me so long to figure out what was happening. Hopefully next time I run into this I remember to come back here and read this blog post.

Tuesday, April 30, 2019

TDD in JavaScript (Part 3)

This blog post was originally published on the ThoroughTest website, back when that was a thing. As a co-founder and primary content contributor for ThoroughTest, I absolutely own the rights to this post and the source code to which it refers. I intend to reproduce each blog post here on my personal blog since the company is no longer in business.

In our twoprevious posts on this topic we setup Jasmine in preparation for simple browser-based tic-tac-toe game, established our requirements for the game, and wrote our first test and first bit of functionality for the game. In this post we're going to work quickly through the iterative process of test-driven development.

Before we write a bunch of tests we're going to introduce the beforeEach and afterEach functions of Jasmine. As their names describe beforeEach and afterEach are executed before and after each spec (test) in a describe block, respectively. We're going to migrate some pieces from our first test into these functions so we can reuse them.

Changing our tests to be more concise, reusable, or more efficient is part of the very important refactoring step in test-driven development.
describe('selectBox', () => {
  beforeEach(() => {
    const elementToCreate = document.createElement('button');
    elementToCreate.id = 'topLeft';
    elementToCreate.innerHTML = ' ';
    elementToCreate.onclick = selectBox(elementToCreate);
    document.body.appendChild(elementToCreate);
  });

  it('should mark the box with the current players marker', () => {
    // arrange
    const element = document.getElementById('topLeft');

    // act
    element.click();

    // assert
    expect(element.innerHTML).toBe('X');
  });

  afterEach(() => {
    document.body.removeChild(document.getElementById('topLeft'));
  });
});
Now that we've refactored our unit test we can quickly write some more. We recommend writing the unit test blocks before writing all of the tests. Doing so helps us identify potential discrepancies in the requirements and gives us a better understanding of what we're being asked to code.

describe('selectBox', () => {
  beforeEach(() => {
    const elementToCreate = document.createElement('button');
    elementToCreate.id = 'topLeft';
    elementToCreate.innerHTML = ' ';
    elementToCreate.onclick = selectBox(elementToCreate);
    document.body.appendChild(elementToCreate);
  });

  it('should mark the box with the current players marker', () => {
    // arrange
    const element = document.getElementById('topLeft');

    // act
    element.click();

    // assert
    expect(element.innerHTML).toBe('X');
  });
  
  it('should become Os turn when X marks a box', () => {});
 
  it('should mark the box with O when the current player is X', () => {});
  
  it('should become Xs turn when O marks a box', () => {});

  it('should end the game when O marks three boxes in a row', () => {});

  it('should end the game when X marks three boxes in a column', () => {});

  it('should end the game when O marks three boxes diagonally', () => {});

  it('should end the game when all boxes are marked and neither player has won',
    () => {});

  afterEach(() => {
    document.body.removeChild(document.getElementById('topLeft'));
  });
});
While we were writing our tests we noticed that our requirements never specified how to start a new game, whether to keep track of how many wins each player had or how many games ended in a draw. We're going to move forward without these features, but it's good that we identified them now. If the product owner really wanted them, we would have been able to renegotiate the work we're doing to make sure we included the most important features in the first release.

Now that we have our tests started we can start writing them. For the sake of brevity, we've written them all and included them here.

describe('selectBox', () => {
  beforeEach(() => {
    const elementToCreate = document.createElement('button');
    elementToCreate.id = 'topLeft';
    elementToCreate.innerHTML = ' ';
    elementToCreate.onclick = selectBox(elementToCreate);
    document.body.appendChild(elementToCreate);
  });

  it('should mark the box with X when the current player is O', () => {
    // arrange
    const element = document.getElementById('topLeft');

    // act
    element.click(element);

    // assert
    expect(element.innerHTML).toBe('X');
  });

  it('should become Os turn when X marks a box', () => {
    // arrange
    player = 'X';
    const element = document.getElementById('topLeft');

    // act
    element.click(element);

    // assert
    expect(player).toBe('O');
  });

  it('should mark the box with O when the current player is X', () => {
    // arrange
    player = 'O';
    const element = document.getElementById('topLeft');

    // act
    element.click(element);

    // assert
    expect(element.innerHTML).toBe('O');
  });

  it('should become Xs turn when O marks a box', () => {
    // arrange
    player = 'O';
    const element = document.getElementById('topLeft');

    // act
    element.click(element);

    // assert
    expect(player).toBe('X');
  });

  it('should end the game when O marks three boxes in a row', () => {
    // arrange
    player = 'O';
    moves = ['', 'O', 'O', 'X', 'X', '', 'X', '', ''];
    
    // act
    element.click(element);

    // assert
    expect(document.getElementById('gameStatus').innerHTML).toBe('<h1>O Wins!</h1>');
  });

  it('should end the game when X marks three boxes in a column', () => {
    // arrange
    player = 'X';
    moves = ['', 'O', 'O', 'X', 'X', '', 'X', '', ''];

    // act
    element.click(element);

    // assert
    expect(document.getElementById('gameStatus').innerHTML).toBe('<h1>X Wins!</h1>');
  });

  it('should end the game when O marks three boxes diagonally', () => {
    // arrange
    player = 'O';
    moves = ['', 'X', 'X', 'X', 'O', '', '', '', 'O'];

    // act
    element.click(element);

    // assert
    expect(document.getElementById('gameStatus').innerHTML).toBe('<h1>O Wins!</h1>');
  });

  it('should end the game when all boxes are marked and neither player has won',
    () => {
      // arrange
      player = 'X';
      moves = ['', 'X', 'O', 'O', 'O', 'X', 'X', 'O', 'X'];

      // act
      element.click(element);

      // assert
      expect(  
        document.getElementById('gameStatus')
        .innerHTML).toBe('<h1>Cat\'s Game!</h1>');
  });
  
  afterEach(() => {
    document.body.removeChild(document.getElementById('topLeft'));
  });
});
All of our tests fail except the first one, which is perfectly normal. Now we write just a little bit of code, using our tests as our guides.

let moves = ['', '', '', '', '', '', '', '', ''];
let player = 'X';

function selectBox() {
  this.innerHTML = player;

  if (this.id === 'topLeft') {
    moves[0] = player;
  } else if (this.id === 'topMiddle') {
    moves[1] = player;
  } else if (this.id === 'topRight') {
    moves[2] = player;
  } else if (this.id === 'centerLeft') {
    moves[3] = player;
  } else if (this.id === 'centerMiddle') {
    moves[4] = player;
  } else if (this.id === 'centerRight') {
    moves[5] = player;
  } else if (this.id === 'bottomLeft') {
    moves[6] = player;
  } else if (this.id === 'bottomMiddle') {
    moves[7] = player;
  } else if (this.id === 'bottomRight') {
    moves[8] = player;
  }

  if (moves[0] === moves[1] && moves[1] === moves[2]) {
    document.getElementById('gameStatus').innerHTML = '<h1>' + player + ' Wins!</h1>';
  } else if (moves[0] === moves[3] && moves[3] === moves[6]) {
    document.getElementById('gameStatus').innerHTML = '<h1>' + player + ' Wins!</h1>';
  } else if (moves[0] === moves[4] && moves[4] === moves[8]) {
    document.getElementById('gameStatus').innerHTML = '<h1>' + player + ' Wins!</h1>';
  } else {
    document.getElementById('gameStatus').innerHTML = '<h1>Cat\'s Game!</h1>';
  }

  player = player === 'X' ? 'O' : 'X';
}
This function causes all of our tests to pass, but doesn't mean the game will work. We're only checking whether the top row is a win, or the left column, or diagonally from top left to bottom right. If someone marks all three boxes in the middle row they won't win. We'll need to go back and write more tests, then update our code for the tests to pass.

We've added the additional tests we needed and we also refactored some of our test code to include some helper methods to create and remove all of the buttons from the form for us for each test.

describe('selectBox', () => {
  function createButton(id) {
    const elementToCreate = document.createElement('button');
    elementToCreate.id = id;
    elementToCreate.innerHTML = ' '
    elementToCreate.onclick = selectBox;
    document.body.appendChild(elementToCreate);
  }

  beforeEach(() => {
    createButton('topLeft');
    createButton('topMiddle');
    createButton('topRight');
    createButton('centerLeft');
    createButton('centerMiddle');
    createButton('centerRight');
    createButton('bottomLeft');
    createButton('bottomMiddle');
    createButton('bottomRight');

    const gameStatusElement = document.createElement('div');
    gameStatusElement.id = 'gameStatus';
    document.body.appendChild(gameStatusElement);
  });

  it('should mark the box with X when the current player is O', () => {
    // arrange
    const element = document.getElementById('topLeft');

    // act
    element.click();

    // assert
    expect(element.innerHTML).toBe('X');
  });

  it('should become Os turn when X marks a box', () => {
    // arrange
    player = 'X';
    const element = document.getElementById('topLeft');

    // act
    element.click();

    // assert
    expect(player).toBe('O');
  });

  it('should mark the box with O when the current player is X', () => {
    // arrange
    player = 'O';
    const element = document.getElementById('topLeft');

    // act
    element.click();

    // assert
    expect(element.innerHTML).toBe('O');
  });

  it('should become Xs turn when O marks a box', () => {
    // arrange
    player = 'O';
    const element = document.getElementById('topLeft');

    // act
    element.click();

    // assert
    expect(player).toBe('X');
  });

  it('should end the game when O marks three boxes in the top row', () => {
    // arrange
    player = 'O';
    moves = ['', 'O', 'O', 'X', 'X', '', 'X', '', ''];
    const element = document.getElementById('topLeft');
    
    // act
    element.click();

    // assert
    expect(document.getElementById('gameStatus').innerHTML).toBe('<h1>O Wins!</h1>');
  });

  it('should end the game when X marks three boxes in the middle row', () => {
    // arrange
    player = 'X';
    moves = ['', 'O', 'O', 'X', 'X', '', 'X', 'O', ''];
    const element = document.getElementById('centerRight');
    
    // act
    element.click();

    // assert
    expect(document.getElementById('gameStatus').innerHTML).toBe('<h1>X Wins!</h1>');
  });

  it('should end the game when O marks three boxes in the bottom row', () => {
    // arrange
    player = 'O';
    moves = ['', '', 'X', 'X', 'X', '', 'O', '', 'O'];
    const element = document.getElementById('bottomMiddle');
    
    // act
    element.click();

    // assert
    expect(document.getElementById('gameStatus').innerHTML).toBe('<h1>O Wins!</h1>');
  });

  it('should end the game when X marks three boxes in the left column', () => {
    // arrange
    player = 'X';
    moves = ['', 'O', 'O', 'X', 'X', '', 'X', '', ''];
    const element = document.getElementById('topLeft');

    // act
    element.click();

    // assert
    expect(document.getElementById('gameStatus').innerHTML).toBe('<h1>X Wins!</h1>');
  });

  it('should end the game when O marks three boxes in the middle column', () => {
    // arrange
    player = 'O';
    moves = ['X', 'O', '', 'X', '', 'X', '', 'O', ''];
    const element = document.getElementById('centerMiddle');

    // act
    element.click();

    // assert
    expect(document.getElementById('gameStatus').innerHTML).toBe('<h1>O Wins!</h1>');
  });

  it('should end the game when X marks three boxes in the right column', () => {
    // arrange
    player = 'X';
    moves = ['O', 'X', 'X', 'O', '', 'X', '', 'O', ''];
    const element = document.getElementById('bottomRight');

    // act
    element.click();

    // assert
    expect(document.getElementById('gameStatus').innerHTML).toBe('<h1>X Wins!</h1>');
  });

  it('should end the game when O marks three boxes diagonally from top left to bottom right', () => {
    // arrange
    player = 'O';
    moves = ['', 'X', 'X', 'X', 'O', '', '', '', 'O'];
    const element = document.getElementById('topLeft');

    // act
    element.click();

    // assert
    expect(document.getElementById('gameStatus').innerHTML).toBe('<h1>O Wins!</h1>');
  });

  it('should end the game when X marks three boxes diagonally from top right to bottom left', () => {
    // arrange
    player = 'X';
    moves = ['O', 'O', 'X', '', 'X', '', '', 'X', 'O'];
    const element = document.getElementById('bottomLeft');

    // act
    element.click();

    // assert
    expect(document.getElementById('gameStatus').innerHTML).toBe('<h1>X Wins!</h1>');
  });

  it('should not end the game when not all boxes are marked and neither player has won', () => {
    // arrange
    player = 'O';
    moves = ['', 'X', 'O', '', 'O', 'X', 'X', 'O', 'X'];
    const element = document.getElementById('topLeft');

    // act
    element.click();

    // assert
    expect(document.getElementById('gameStatus').innerHTML).toBe('');
  });

  it('should end the game when all boxes are marked and neither player has won', () => {
    // arrange
    player = 'X';
    moves = ['', 'X', 'O', 'O', 'O', 'X', 'X', 'O', 'X'];
    const element = document.getElementById('topLeft');

    // act
    element.click();

    // assert
    expect(document.getElementById('gameStatus').innerHTML).toBe('<h1>Cat\'s Game!</h1>');
  });
  
  afterEach(() => {
    document.body.removeChild(document.getElementById('topLeft'));
    document.body.removeChild(document.getElementById('topMiddle'));
    document.body.removeChild(document.getElementById('topRight'));
    document.body.removeChild(document.getElementById('centerLeft'));
    document.body.removeChild(document.getElementById('centerMiddle'));
    document.body.removeChild(document.getElementById('centerRight'));
    document.body.removeChild(document.getElementById('bottomLeft'));
    document.body.removeChild(document.getElementById('bottomMiddle'));
    document.body.removeChild(document.getElementById('bottomRight'));
    document.body.removeChild(document.getElementById('gameStatus'));
  });
});
With these updated tests we'll need to update our code, so we've done that as well.

let moves = ['', '', '', '', '', '', '', '', ''];
let player = 'X';


function selectBox() {
  this.innerHTML = player;

  if (this.id === 'topLeft') {
    moves[0] = player;
  } else if (this.id === 'topMiddle') {
    moves[1] = player;
  } else if (this.id === 'topRight') {
    moves[2] = player;
  } else if (this.id === 'centerLeft') {
    moves[3] = player;
  } else if (this.id === 'centerMiddle') {
    moves[4] = player;
  } else if (this.id === 'centerRight') {
    moves[5] = player;
  } else if (this.id === 'bottomLeft') {
    moves[6] = player;
  } else if (this.id === 'bottomMiddle') {
    moves[7] = player;
  } else if (this.id === 'bottomRight') {
    moves[8] = player;
  }

  if (moves[0] === player && moves[1] === player && moves[2] === player) {
    // top row
    document.getElementById('gameStatus').innerHTML = '<h1>' + player + ' Wins!</h1>';
  } else if (moves[3] === player && moves[4] === player && moves[5] === player) {
    // middle row
    document.getElementById('gameStatus').innerHTML = '<h1>' + player + ' Wins!</h1>';
  } else if (moves[6] === player && moves[7] === player && moves[8] === player) {
    // bottom row
    document.getElementById('gameStatus').innerHTML = '<h1>' + player + ' Wins!</h1>';
  } else if (moves[0] === player && moves[3] === player && moves[6] === player) {
    // left column
    document.getElementById('gameStatus').innerHTML = '<h1>' + player + ' Wins!</h1>';
  } else if (moves[1] === player && moves[4] === player && moves[7] === player) {
    // middle column
    document.getElementById('gameStatus').innerHTML = '<h1>' + player + ' Wins!</h1>';
  } else if (moves[2] === player && moves[5] === player && moves[8] === player) {
    // right column
    document.getElementById('gameStatus').innerHTML = '<h1>' + player + ' Wins!</h1>';
  } else if (moves[0] === player && moves[4] === player && moves[8] === player) {
    // top left to bottom right
    document.getElementById('gameStatus').innerHTML = '<h1>' + player + ' Wins!</h1>';
  } else if (moves[2] === player && moves[4] === player && moves[6] === player) {
    // top right to bottom left
    document.getElementById('gameStatus').innerHTML = '<h1>' + player + ' Wins!</h1>';
  } else if (!!moves[0] && !!moves[1] && !!moves[2] &&
    !!moves[3] && !!moves[4] && !!moves[5] &&
    !!moves[6] && !!moves[7] && !!moves[8]) {
      document.getElementById('gameStatus').innerHTML = '<h1>Cat\'s Game!</h1>';
  }

  player = player === 'X' ? 'O' : 'X';
}
And now our game of Tic Tac Toe should work exactly as we planned. The only part left is creating the actual markup for the game board. Since that's not really TDD we're going to show you what we used, but not go into any great detail about it. Here's our HTML file.

<html>
 <head>
  <script src="./src/tic-tac-toe.js"></script>
  <style>
   #board {
    height: 50%;
    width: 100%;
    position: relative;
   }

   button {
    font-size: 3em;
    width: 75px;
    height: 75px;
    position: relative;
    float: left;
   }

   #centerLeft, #bottomLeft, #gameStatus {
    clear: left;
   }

   #gameStatus {
    height: 10%;
    text-align: center;
   }
  </style>
 </head>
 <body>
  <div id="board"></div>
  <div id="gameStatus"></div>
  <script>createForm()</script>
 </body>
</html>
We're creating the controls for the form dynamically so we'll show you that part, too (the createForm() function called at the bottom of the markup you see up there).
It's important to note that we skipped doing TDD on the createForm and createButton functions for this guide, but if we were really developing a game like this we absolutely would have.


let buttons = ['topLeft', 'topMiddle', 'topRight', 'centerLeft', 'centerMiddle', 'centerRight', 'bottomLeft', 'bottomMiddle', 'bottomRight'];

function createButton(id) {
  const button = document.createElement('button');
  button.innerHTML = ' ';
  button.onclick = selectBox;
  button.id = id;
  return button;
}

function createForm() {
  const board = document.getElementById('board');
  for (let i = 0; i < buttons.length; i++) {
    const button = createButton(buttons[i]);
    board.appendChild(button);
  }
}
These two functions are included in our tic-tac-toe.js file for easier inclusion in the page.

That's the end of the guide on using test-driven development for your JavaScript. Happy coding!


Monday, April 29, 2019

TDD in JavaScript (Part 2)

This blog post was originally published on the ThoroughTest website, back when that was a thing. As a co-founder and primary content contributor for ThoroughTest, I absolutely own the rights to this post and the source code to which it refers. I intend to reproduce each blog post here on my personal blog since the company is no longer in business.


In the previous post we setup Jasmine in preparation for a simple browser-based tic-tac-toe game. In this post we're going to actually write some tests and do some test-driven development for that game. First we need our requirements. Remember that in test-driven development our tests are written against the requirements and not the actual code. Without requirements we don't have any tests.

  1. In a game of tic-tac-toe we have a game board that has nine boxes arranged in a 3x3 pattern (3 columns and 3 rows)
  2. Our two players are Xs and Os.Xs go first.When X clicks on a box, that box is marked with an X and it becomes O's turn
  3. When O clicks on a box, that box is marked with an O and it becomes X's turn
  4. There are four possible outcomes to a game:
    • One player marks all three boxes in the same row
    • One player marks all three boxes in the same column
    • One player marks three boxes in a row diagonally
    • All boxes are selected and no player has achieved any of the other three outcomes
The rules are pretty straight forward so we should be able to generate some unit tests pretty easily from them.

A quick side not about Jasmine. Jasmine is a behavior-driven development framework, which implies a lot of things that aren't important to this post, but are important. We bring it up because our tests will be named differently than we've named them before. In Jasmine, each test is called a spec and the specs are grouped in logical blocks called describes. We recommend creating a new describe for each function you're going to test. Let's get set up to write our first spec.

I've created two new folders in tddjs: src and tests. In the tests folder, I'll create a new file called tic-tac-toe.spec.js. Because our application should be very simple we'll keep all of our specs in a single file. I'll also create a new file in the src folder called tic-tac-toe.js. Now that we have those two files (even though they're empty) we'll want to modify the SpecRunner.html file we got from Jasmine.

If you open SpecRunner.html you should see two sections marked with comments that indicate where you should include your source files and your spec files. Right now they likely reference files that we deleted earlier. We'll replace what's there with references to our two new files. SpecRunner.html now looks like this.

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Jasmine Spec Runner v2.8.0</title>

  <link rel="shortcut icon"
      type="image/png" href="lib/jasmine-2.8.0/jasmine_favicon.png">
  <link rel="stylesheet" href="lib/jasmine-2.8.0/jasmine.css">

  <script src="lib/jasmine-2.8.0/jasmine.js"></script>
  <script src="lib/jasmine-2.8.0/jasmine-html.js"></script>
  <script src="lib/jasmine-2.8.0/boot.js"></script>

  <!-- include source files here... -->
  <script src="src/tic-tac-toe.js"></script>

  <!-- include spec files here... -->
  <script src="tests/tic-tac-toe.spec.js"></script>

</head>

<body>
</body>
</html>
Let's go ahead and open SpecRunner.html in our browser and take a look at what we have so far.



You should have something that looks very similar to this. We don't have any specs (tests) yet so nothing ran. That's fine. Now it's time to write our first test. Remember that in test-driven development our tests will always fail first.

We'll create a describe for a function we'll call selectBox in tic-tac-toe.spec.js.

describe('selectBox', () => {});
There's not much to see there because we still don't have our first spec written. We can add it easily enough.

describe('selectBox', () => {
  it('should mark the box with the current players marker', () => {});
});
Our Jasmine tests are named a little bit more plainly than our other types of tests. The language and constructs in Jasmine are designed to read like English. That is, our test should actually be read like this: "selectBox should mark the box with the current players marker". That allows us to easily correlate our tests with our requirements and share those results with our business customers if we want. This is a feature of behavior-driven development.

If you refresh SpecRunner.html in your browser you should now see a message indicating that you have a spec, it passed, but it has no expectations. That's great! It's not actually passing, but it's not failing either. Let's change our test to make it fail.

describe('selectBox', () => {
  it('should mark the box with the current players marker', () => {
 // arrange
 const element = document.getElementById('topLeft');
 element.innerHTML = ' ';
 
 // act
 element.click();
 
 // assert
 expect(element.innerHTML).toBe('X');
  });
});
This test simply tries to get an element from the page that has an id of topLeft. We make sure the element isn't already marked with anything by setting its innerHTML property to a non-breaking space. We click the element, then check to make sure its innerHTML property is now set to X. When we refresh SpecRunner.html we should now see that we have one failing test.


Jasmine tells us right away not only that we have a failing test, but also why our test is failing. In this case we can see that our test is failing because we tried to access the innerHTML property of an element that didn't exist. This is where things get a little bit more complicated. What we need to do now is create an element with the id we're looking for before our test runs.

describe('selectBox', () => {
  it('should mark the box with the current players marker', () => {
    // arrange
    const elementToCreate =  document.createElement('button');
    elementToCreate.id = 'topLeft';
    elementToCreate.innerHTML = ' '
    document.body.appendChild(elementToCreate);
    const element =  document.getElementById('topLeft');
 
    // act
    element.click();
 
    // assert
    expect(element.innerHTML).toBe('X');
  });
});
With this change we're using JavaScript to programmatically add a button to our document so we can access it and click on it later. Our test still fails because clicking on our button doesn't actually do anything yet.


This time we see a couple of new things on SpecRunner.html. First, we see that the reason for the failed test has changed. We now see that the failure is caused by our expectation not being met. This is good news because it means our test is running properly, but what we expect to happen isn't happening. The second thing we see that's new is there is now a button on the document in SpecRunner.html. You can see it there in the bottom left of the screenshot.

Let's add the functionality to give our new button a click event and tell it to invoke the function selectBox when it is clicked. We'll want to pass the element being clicked into the selectBox function. We'll also add a little bit of cleanup to remove the button from the document when our test finishes. This helps keep SpecRunner clean, but it will also have greater implications as we add more tests.

describe('selectBox', () => {
  it('should mark the box with the current players marker', () => {
    // arrange
    const elementToCreate =  document.createElement('button');
    elementToCreate.id = 'topLeft';
    elementToCreate.innerHTML = ' '
    elementToCreate.onclick = selectBox(elementToCreate);
    document.body.appendChild(elementToCreate);
    const element =  document.getElementById('topLeft');
 
    // act
    element.click(element);
 
    // assert
    expect(element.innerHTML).toBe('X');

    // cleanup
    document.body.removeChild(elementToCreate);
  });
});
Our test still fails, but this time it's failing for exactly the right reason: there is no selectBox function. Also notice that the button is not showing up anymore.


Since we're going to do this iteratively (like we're supposed to) we can go ahead and add a selectBox function to our tic-tac-toe.js file.

function selectBox() { }
We're back to having a failing test because our expectation is not being met. That makes sense because nothing is happening in our actual code yet. Now we can add the code that updates the innerHTML property of the element that was clicked.

function selectBox(element) {
    element.innerHTML = 'X';
}
Our test is passing now! Astute readers will notice that our code doesn't currently correctly implement our requirements, but that's OK for right now. Right now we've written a single test that tests a single piece of code and that test passes.

We're going to take another break here. At this point we know how to setup Jasmine, write our first test, and get it to pass. In the next installment we'll add more tests and try to get closer to having a working game of Tic Tac Toe.


Sunday, April 28, 2019

TDD in JavaScript (Part 1)

This blog post was originally published on the ThoroughTest website, back when that was a thing. As a co-founder and primary content contributor for ThoroughTest, I absolutely own the rights to this post and the source code to which it refers. I intend to reproduce each blog post here on my personal blog since the company is no longer in business.


We've heard from some front-end developers that test-driven development isn't possible or useful for them. We (obviously) couldn't disagree more so we decided to put together a quick guide to using test-driven development to create a pure JavaScript application. We prefer the Jasmine testing framework so that's what we're going to demonstrate here.

First off, if this is the first time you've heard of Jasmine you might want to check out their official site (the link is in the Resources section at the bottom of this post).

Whether you want to check out their site or not, you're going to need to go there to get the framework. Don't worry, it's a small download and they have it available in .zip and .tar format. You can just go straight to the link in the Resources section if you don't want to browse their site.

To keep things simple I created a directory called "tddjs" on my desktop and unzipped the file I downloaded straight into that directory. Here's what my folder looks like.


Their download now includes some sample code of their own (in the src and spec folders), but we don't need that. For this guide we are only interested in keeping the contents of the lib/jasmine-2.8.0 directory and the SpecRunner.html file in the root. I'm going to go ahead and delete the other files. Here's what my folder looks like now.

Looking into the lib/jasmine-2.8.0 folder we see 6 files:

boot.js

console.js

jasmine.css

jasmine.js

jasmine-favicon.png

jasmine-html.js

jasmine.js contains the actual Jasmine testing framework, jasmine-html.js contains some functions that help format the page, and boot.js initializes Jasmine.

Now that we have Jasmine ready we need some code to test. *record scratch* Oops. We need some tests. We're going to create a simple tic-tac-toe game. This should allow us to focus on the functionality we expect instead of getting bogged down in how things look. This is actually a really good stopping point so we're going to break here. We should be able to wrap things up in our next post.

Resources

Official Jasmine Home Page

Download Jasmine


Tuesday, July 17, 2018

Angular Error: Illegal state: Could not load the summary for directive SomeComponent

Apparently I had encountered this issue before, but it came up again and I had to research it again (I only know I came across it before because the link to the answer on SO was purple).

When your tests fail with this message, make sure you're including the component under test (SomeComponent) in the declarations of the TestBed. It's super simple, but apparently it's bitten me at least twice.

Wednesday, July 11, 2018

Angular Tests: Error during cleanup of component

I've come across this error quite a few times and it always takes me a few minutes to remember how to overcome it so I figured I should write about it.

This error typically arises for me when I subscribe to an observable in my ngOnInit, but then don't unsubscribe in ngOnDestroy. That's it. If you (or Future Me) start seeing this error - followed by a ton of text written to the console - you may want to check if you've added a .subscribe in ngOnInit and then unsubscribe in your ngOnDestroy.

Thursday, April 19, 2018

Testing Angular 5 Promises and Tick

I have a function that calls two other functions that return promises. Each promise has a callback (.then) chained to it, which will also return another promise (because that's how promises work). Once both promises are resolved and their callbacks are complete, I take one final action. It looks like this:
   1:  const promises: Promise<void>[] = [];
   2:  promises.push(this.service.doTheFirstThingThatReturnsAPromise().then(result => {
   3:    this.firstProp = true;
   4:  }));
   5:
   6:  promises.push(this.service.doTheSecondThingThatReturnsAPromise().then(result => {
   7:    this.secondProp = true;
   8:  }));
   9:
  10:  Promise.all(promises).then(() => {
  11:    this.thirdProp = true;
  12:  });

My problem arose when I tried to test this. It seemed like everything should have been working fine with this test:
   1:  beforeEach(() =>{
   2:    component.doTheThingThatDoesTheOtherThings();
   3:  });
   4:
   5:  it('should do some stuff', fakeAsync(()=>{
   6:    tick();
   7:    expect(component.firstProp).toBe(true);
   8:    expect(component.secondProp).toBe(true);
   9:    expect(component.thirdProp).toBe(true);
  10:  }));

I've done almost exactly this before (in this same project no less!) and it worked fine. The only difference was that in this case I decided to move my target function invocation inside my beforeEach and that made all the difference. With the function invocation happening inside the beforeEach, my entire test was completing before the tick was ever process (which I still don't fully understand, which is part of the reason I'm writing this up). All I had to do to get my tests to pass was move the target function invocation into my spec instead of the beforeEach. So my final (working) result looks like this:
   1:  it('should do some stuff', fakeAsync(()=>{
   2:    component.doTheThingThatDoesTheOtherThings();
   3:    tick();
   4:    expect(component.firstProp).toBe(true);
   5:    expect(component.secondProp).toBe(true);
   6:    expect(component.thirdProp).toBe(true);
   7:  }));

Fortunately, that only took me about 30 minutes to figure out. Hopefully next time I encounter something similar I check back here first and save myself that 30 minutes.

Tuesday, March 20, 2018

The Unhelpful [object ErrorEvent] Message When Testing Angular

A few times in the past two weeks my tests have started failing with a message that was rather less than helpful. Instead of indicating what might actually be the problem somewhere I simply see
"[object ErrorEvent]", which is right on par with the wonderful "object reference not set to an instance of an object" exception in .Net.

The first time I encountered this issue, I found a blog that lead me to the right answer. You can read the other guy's post here. This time around, the error message was the same, but the solution was different. Since this error obviously has more than one underlying problem, I was very glad to stumble upon an SO answer (here) that said to restart the tests with the --sourcemaps=false flag.

As soon as my test ran with that flag set that way I got a helpful error message that lead me to the root of the problem: "view.root.sanitizer.sanitize is not a function". If you're having the same problem, try setting the sourcemaps flag to false when you run your tests and see if the better exception message allows you to hunt down what's happening.

Oh, since I know this will come up for me again, the actual problem I was having was caused by the DomSanitizer service. I'm injecting a mock service in the constructor for my tests, but my mock service only had the function I was actually calling in it. Since DomSanitizer extends Sanitizer there should have also been a sanitize function on my mock. The solution ended up being adding a sanitize function, but because of the terrible, unhelpful error message it took me several hours to figure that out.

Monday, March 19, 2018

Testing ViewChildren in Angular

I have a component that uses a multi-select-dropdown (MSD) component. There can be many MSDs on a single page. When one of the MSDs is opened, I want to close all of the others. There's a function called close on each MSD, and each MSD also has an id property. Finally, the MSDs are created dynamically based on some remote data (so they don't have template reference names).

So how do I close every MSD on the page except the one I'm currently trying to open? It turns out I can get references to all of the MSDs on the page by using ViewChildren. I have a property on my component (my page component) that looks like this
@ViewChildren(MultiSelectDropdownComponent) dropdowns: QueryList<MultiSelectDropdownComponent>;
Now that I know how to get access to them, I have a new problem: how to I populate the dropdowns property from my unit tests?

It turns out that's really easy, too. I just need to let the TestBed do its thing. Since my MSDs are created using an ngFor on another property on my page component (called items), all I have to do is populate my items property, then rebuild the test component. That will build the MSDs for me. That looks like this:
const spies = [];
component.items = [
 {id: 1, options: [...]},
 {id: 2, options: [...]},
 {id: 3, options: [...]}
];
fixture.detectChanges();
component.dropdowns.forEach(d => {
  spies.push({
    id: d.id,
    spy: spyOn(d, 'close')
  });
});
When we invoke fixture.detectChanges our markup is rebuilt, which generates our three MSDs on the test form. Once that happens, the MSDs are available in the dropdowns property so I can iterate through the dropdowns property and spy on the close function of each separate MSD.

Now that I'm spying on the close function of each MSD, I want to invoke my function under test (called toggleOthers) and make sure that all the other MSDs are closed, but not the one that invoked toggleOthers in the first place (because I'm actually trying to open that one).
component.toggleOthers(2);

spies.forEach(s => {
  if (s.id === 2) {
    expect(s.spy).not.toHaveBeenCalled();
  } else {
    expect(s.spy).toHaveBeenCalledTimes(1);
  }
}
Since we invoked the toggleOthers function with the id we're opening, we can check to make sure the MSD associated with that id isn't closed. Then we can check that all other MSDs were closed.

The caveat to this approach is that if the MSD was complex we wouldn't really want to bring the real MSD into the tests just so we could do this. I'm sure that'll come up for me someday so if it does I'll try to remember to write a post on that and update this one with a link to it. If that's what you're looking for, sorry. Hopefully this helps you get there.

Friday, March 16, 2018

Testing Angular Components That Use Pipes

Pipes are a common feature in Angular that are often used to do things like format data (read more about Pipes in the Angular documentation). Unfortunately, when we use Pipes in our Components they can cause some issues with testing. In order to test a Component that uses a Pipe you need to either bring the Pipe in to the TestBed or (my preference) create a Fake Pipe and instruct your test to use that instead.

I prefer this method because it means I can control the functionality of the Pipe, which usually means replacing whatever the Pipe actually does with nothing. I can also more easily manipulate my tests to provide expected outputs, which is always helpful.

Let's say you have a Component whose template uses a Pipe to translate something. The real TranslatePipe accepts a parameter called "query", looks up the query value in a file, and returns the value found in the file. It's basically a key/value lookup. When we're unit testing we want to make sure we're testing the smallest possible unit, which means we should have already tested the real TranslatePipe. Since we can trust that the real TranslatePipe works as expected, we don't want to actually use the TranslatePipe in our Component tests. Instead we'll create a fake TranslatePipe.

When we use the Angular CLI to generate our components we also get a spec file for that component, which has some boilerplate code in it that probably looks something like this:
/* Import statements up here */

describe('CoolComponent', () => {
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [CoolComponent]
    })
    .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(CoolComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });
});
Let's say the very first thing we're going to do on our CoolComponent is use our TranslatePipe to display a title on the page. We know the title of the page will need to be translated and we know we have the TranslatePipe. What we'll want to test is whether the TranslatePipe is invoked during rendering.
it('should display translated title', () => {});
Before we can actually write our test we'll need to create our FakeTranslatePipe and instruct the TestBed to use it instead of the real TranslatePipe. In the same file*, before the describe block, but after the imports, add the following code:
@Pipe({name: 'translate'})
export class FakeTranslatePipe implements PipeTransform {
  transform(query: string): any {
    return 'Translated!';
  }
}
Now that we've created the FakeTranslatePipe, we'll instruct the TestBed to use it by supplying it to the declarations array. The updated beforeEach looks like this:
beforeEach(async(() => {
  TestBed.configureTestingModule({
    declarations: [
      CoolComponent,
      FakeTranslatePipe
    ]
  })
  .compileComponents();
}));
When our component markup uses the translate pipe, our FakeTranslatePipe will be used instead of the real TranslatePipe. That's all we have to do to avoid an error, but if we want to test whether the Pipe was actually invoked during rendering we'll need to create a spy on it. Create a variable for the spy inside the describe block, but outside the first beforeEach.
let pipeSpy: jasmine.Spy;
After we compile the components, but while we're still inside the first beforeEach, we'll spy on FakeTranslatePipe's translate function. Because we're spying on a function that exists on a class instead of an instance we'll have to spy on the prototype of FakeTranslatePipe. It's easy enough to do once you know to do it. The spy creation looks like this:
pipeSpy = spyOn(FakeTranslatePipe.prototype, 'transform').and.callThrough();
Now we can treat pipeSpy like other spy (that is, we can check whether it was called, how many times, with what parameters, etc.) in our tests. Here's what our test might look like now:
it('should display translated title', () => {
  expect(pipeSpy).toHaveBeenCalledTimes(1);
  expect(pipeSpy).toHaveBeenCalledWith('page-title');
});
We're really close to being finished, but all we know is that our FakeTranslatePipe was invoked. We don't know whether we put the result in an h1 tag. We can add that to our test so the end result looks like this:
it('should display translated title', () => {
  const debugElements = fixture.debugElement.queryAll(By.css('h1'));

  expect(debugElements.length).toBe(1);
  expect(debugElements[0].nativeElement.textContent).toBe('Translated!');
  expect(pipeSpy).toHaveBeenCalledTimes(1);
  expect(pipeSpy).toHaveBeenCalledWith('page-title');
});
The last thing I'll leave you with is the final version of the whole spec file.
import { Pipe, PipeTransform } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';

import { CoolComponent } from './cool-component';

@Pipe({name: 'translate'})
export class FakeTranslatePipe implements PipeTransform {
  transform(query: string): any {
    return 'Translated!';
  }
}

describe('CoolComponent', () => {
  let pipeSpy: jasmine.Spy;
  let component: CoolComponent;
  let fixture: ComponentFixture<CoolComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [
        CoolComponent,
        FakeTranslatePipe
      ]
    })
    .compileComponents();
    
    pipeSpy = spyOn(FakeTranslatePipe.prototype, 'transform').and.callThrough();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(CoolComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should display translated title', () => {
    const debugElements = fixture.debugElement.queryAll(By.css('h1'));
    
    expect(debugElements.length).toBe(1);
    expect(debugElements[0].nativeElement.textContent).toBe('Translated!');
    expect(pipeSpy).toHaveBeenCalledTimes(1);
    expect(pipeSpy).toHaveBeenCalledWith('page-title');
  });
});



*If we know we'll be using the same FakeTranslatePipe in lots of different spec files throughout the project, we could create it in a separate file and import it, but for the purposes of this post we're going to create it in the same file.

Saturday, February 11, 2017

Testing Resolves in Jasmine

I've come across this issue before and I came up with a different answer (I think), but I can't remember that answer and I can't find it here.  So today I'm posting my new answer so at least I have this one for next time.

I'm using Angular UI Bootstrap's modal control to... open a modal.  I suppose that's kind of obvious.  Anyway, when you open a modal you can pass functions in that get resolved in the controller.  I'm not going to explain that in great detail here.

My problem was testing those resolves in my modal instance controller.  The resolves look something like this:
resolve: {
    student: function() {
        return {};
    },
    title: function() {
        return 'Add a Student';
    }
}

Since they're functions, UI Bootstrap is going to handle resolving them (get it?) to their returned values.  So when the controller loads, student will be an empty object and title will be that text.  When I test the modal instance controller I'll just be sure to inject those resolved values instead of functions.  It looks like this:
beforeEach(inject(function(_$rootScope_, _$controller_) {
    scope = _$rootScope_.$new();

    _$controller_('studentController', { $scope: scope, title: 'Add a Student', meal: {} });
}));

Now in my tests I can use those values the same way they'll be used in the actual code.  I answered a question on Stack Overflow about this topic a while back, but I can't recall what code I was looking at that prompted me to go looking for an answer that lead me to that question that I was able to answer.  Anyway, here's my answer on SO: http://stackoverflow.com/questions/37023953/unit-testing-ui-router-with-resolve/37075491#37075491

Tuesday, December 20, 2016

Testing "this" with Jasmine

I had cause to test a function that references this in JavaScript and, although it ended up being easy, it took some figuring to get there.  Rather than have to figure it all out again (and maybe to save someone else some trouble, too) I'm putting my solutions here (yes, there are two solutions).

Here is the function I needed to test:
$scope.dataBound = function(e) {
    var data = this.dataSource.data();
    for (var i = data.length; --i >= 0;) {
        if (!data[i].IsBundle) {
            var row = $('#grid')
                .data('kendoGrid')
                .tbody.find('tr[data-uid="' + data[i].uid + '"]');
            $(row).find('td.k-hierarchy-cell .k-icon').removeClass();
        }
    }
};

The first - and easiest - solution is to set whatever you need on scope.  In Angular, when this is referenced, it's referring to scope so in my case I was using this.dataSource.data() so I was able to just set an object directly on scope called dataSource that had a data function.
it('should set this on scope', function() {
    // arrange
    scope.dataSource = {
        data: function() {
            return [];
        }
    };
    spyOn(scope.dataSource, 'data').and.callThrough();

    // act
    scope.dataBound();

    // assert
    expect(scope.dataSource.data).toHaveBeenCalledTimes(1);
});

The second - and in my opinion more correct - solution is to invoke the function using call and pass whatever you want this to be as your first parameter.
it('should set pass this using call', function() {
    // arrange
    var thisToSet = {
        dataSource: {
            data: function() {
                return [];
            }
        }
    };
    spyOn(thisToSet.dataSource, 'data').and.callThrough();

    // act
    scope.dataBound.call(thisToSet);

    // assert
    expect(thisToSet.dataSource.data).toHaveBeenCalledTimes(1);
});

They're pretty similar, but I prefer using call because it should work outside Angular as well.

Mozilla has a pretty sweet explanation of this and how to use it in case you want more detailed information.

Friday, June 10, 2016

More Filter Testing

I recently wrote a post on how to test whether a filter was called from a controller.  Today I needed to test whether a filter was called from a factory, which is slightly different.  When we test controllers we instantiate the controller and inject what we want.  That means when we inject the $filter service we can just supply our own spy instead.  In a factory, though, we don't really instantiate the factory.  Instead the factory is just kinda there and we inject other factories and services into it.  (I'm sure there's a way to "instantiate" a factory for testing purposes and I know you don't actually "instantiate" things in JavaScript, but get over it.)  I needed a way to globally tell Angular that during testing I didn't want to use the normal $filter service when it came across it in my factory.  Fortunately, I also recently wrote a post on how to test the $mdSidenav service that's part of Angular Material. I put the two posts together and came up with a solution that works very well.

What I ultimately did was checked into the Angular source code for the $filter service (here) and found that it's pretty straightforward.  It just uses the $injector service to find the registered filter by name and return it.  So I mimicked that in my spy, except that I returned another spy when I came across the filter I wanted to test.  It sounds a bit confusing (even to me) writing it out so why don't you just check out the code below.  That should make more sense.

The code:
angular.module('app', []).factory('myFactory', function($filter) {
  var factory = {};

  factory.states = [
    { name: 'Alabama', id: 'AL' },
    { name: 'Alaskas', id: 'AK' },
    { name: 'Arizona', id: 'AZ' },
    { name: 'Arkansas', id: 'AR' }
  ];
  
  factory.sort = function() {
    return $filter('orderBy')(factory.states, 'id');
  };

  return factory;
});

The spec:
describe('test suite', function() {
  var myFactory, orderByFilterSpy, filterSpy;
  
  beforeEach(module('app'));
  
  beforeEach(module(function($provide, $injector){
    orderByFilterSpy = jasmine.createSpy('orderBy');
    filterSpy = jasmine.createSpy('$filter').and.callFake(function(name) {
      switch(name) {
        case 'orderBy':
          return orderByFilterSpy;
        default:
          return $injector.get(name + 'Filter');
      }
    });

    $provide.factory('$filter', function() {
      return filterSpy;
    });
  }));

  beforeEach(inject(function(_myFactory_) {
    myFactory = _myFactory_;
  });
  
  it('should call orderByFilter', function() {
    // arrange
    myFactory.states = [{id: 'AZ', name: 'Arizona'}, {id: 'AL', name: 'Alabama'}, {id: 'AK', name: 'Alaska'}, {id: 'AR', name: 'Arkansas'}];

    // act
    myFactory.sort();

    // assert
    expect(filterSpy).toHaveBeenCalledWith('orderBy');
    expect(orderByFilterSpy).toHaveBeenCalledWith([{id: 'AZ', name: 'Arizona'}, {id: 'AL', name: 'Alabama'}, {id: 'AK', name: 'Alaska'}, {id: 'AR', name: 'Arkansas'}], 'id');
  });
});

That's it!  As a little added bonus, any filters other than orderBy that happen to get called in my factory should get passed through (I haven't validated that part yet, but it looks like it would do that so I'm rolling with it).

Friday, June 3, 2016

Testing $mdMedia

As I mentioned before I'm working with Angular Material on my project.  Today I had cause to use the $mdMedia service, which accepts a string parameter and returns either true or false based on the screen size.  For example, $mdMedia('lg') will return true if the screen is between 1280px and 1919px.  The service is great, but testing it was - once again - tricky.

I ended up using the same trick I used to test $mdSidenav, but modified it just a little bit.  In the interests of making it easier on myself next time I have to test $mdMedia, here we go.
var mediaMock, mediaQueryResult;
beforeEach(module(function ($provide) {
  mediaMock = jasmine.createSpy('$mdMedia');
  $provide.factory('$mdMedia', function() {
    return function() {
      return mediaQueryResult;
    };
  });
}));

it('should do something when $mdMedia() returns false regardless of what is passed to it', function() {
  // arrange
  mediaQueryResult = false;

  // act
  myFactory.myFunction();

  // assert
  expect(myFactory.myOtherFunction).toHaveBeenCalled();
});

it('should do something when $mdMedia() returns true regardless of what is passed to it', function() {
  // arrange
  mediaQueryResult = true;

  // act
  myFactory.myFunction();

  // assert
  expect(myFactory.myOtherFunction).not.toHaveBeenCalled();
});

That's all I had to do to get it to work.  I'm sure there's a way to vary the result based on the parameter, but I didn't need to do that so I didn't solve that problem... yet.

Tuesday, May 31, 2016

Testing Whether A Filter Was Called

Today I finally circled back on some old tests and decided to figure out how to tell whether a specific filter was called with the correct values.  This has been bothering me for a while, but the answer turned out to be pretty simple.  Let's say you have a controller that calls the built-in orderBy filter and passes an array of states (scope.states) to be sorted by id:
angular.module('app', []).controller('sampleController', function($scope, $filter) {
  $scope.states = [
    { name: 'Alabama', id: 'AL' },
    { name: 'Alaskas', id: 'AK' },
    { name: 'Arizona', id: 'AZ' },
    { name: 'Arkansas', id: 'AR' }
  ];
  
  $scope.sort = function() {
    return $filter('orderBy')(scope.states, 'id');
  };
});

The end result should be that these states get sorted so Alaska comes first, followed by Alabama, Arkansas, then Arizona (AK, AL, AR, AZ).  In order to verify this works as expected we can inject a new spy into the controller instead of using the expected $filter service.  Like this:
describe('test suite', function() {
  var scope, orderByFilterSpy, filterSpy;
  
  beforeEach(module('app'));
  
  beforeEach(inject(function(_$controller_, _$rootScope_) {
    scope = _$rootScope_.$new();
    
    orderByFilterSpy = jasmine.createSpy();
    filterSpy = jasmine.createSpy().and.returnValue(orderByFilterSpy);
    
    _$controller_('sampleController', { $scope: scope, $filter: filterSpy });
  }));
  
  it('should pass scope.states and \'id\' to orderByFilter', function() {
    scope.sort();
    
    expect(filterSpy).toHaveBeenCalled();
    expect(orderByFilterSpy).toHaveBeenCalledWith([
      {name: 'Alabama', id: 'AL'},
      { name: 'Alaskas', id: 'AK' },
      { name: 'Arizona', id: 'AZ' },
      { name: 'Arkansas', id: 'AR' }], 'id');
  });
});

What we end up with is one passing test.  Remember to trust that the orderBy filter does what you're expecting.  That means you're not actually testing whether the result of calling scope.sort is a sorted array.  Instead you just check that you passed the right values to the right filter.  If you're calling a custom filter, make sure to test that filter thoroughly, but separately.