Recreating hasMany relationship in unit test

I have a question regarding unit tests for a model.

The model is a photo album with photos, and the photos can have tags. The photo model has an attribute: amountOfTags.

the amountOfTaggedPhotos getter is supposed to return the amount of photos in the album that have tags

import Model, { hasMany, attr, belongsTo } from '@ember-data/model';

export default class PhotoAlbum extends Model {
  // Properties

  // Relations
  @hasMany photos;

  get amountOfTaggedPhotos() {
    var counter = 0;
    for (var photo of this.photos._objects) {
      counter += photo.amountOfTags > 0 ? 1 : 0;
    }
    return counter;
  }

  get amountOfPhotos() {
    return this.photos.length;
  }
}

this works, but my test doesn’t

import { run } from '@ember/runloop';
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';

let album;
let photos = [];

module('Unit | Model | photo-album', function (hooks) {
  setupTest(hooks);

  hooks.beforeEach(function () {
    album = run(() => this.owner.lookup('service:store').createRecord('PhotoAlbum'));
    photos[0] = run(() => this.owner.lookup('service:store').createRecord('Photo'));
    photos[1] = run(() => this.owner.lookup('service:store').createRecord('Photo'));
    photos[2] = run(() => this.owner.lookup('service:store').createRecord('Photo'));
  });

  test('Photo count', function (assert) {
    assert.expect(1);
    run(() => {
      photos[0].setProperties({amountOfTags: 0});
      photos[1].setProperties({amountOfTags: 1});
      photos[2].setProperties({amountOfTags: 5});
      album.setProperties({photos: photos});
      assert.equal(album.get('amountOfTaggedPhotos'), 2, "Amount of tags in album is correct");
    });
  });
});

Now i know that setProperties({photos: photos}) doesn’t work here, but i don’t know how i should do it instead. Also is the rest of the code okay? or is there a better way of doing this?

Using ember-data relationships are queried via the configured network system. You’d have to mock the network or write your own side-step system. Mocking the network seems easier and you could likely include the relationship data as part of the mock JSON:API data.

I would look into Mirage.js as that is exactly what it is designed for. In fact it originated as a data system for tests in ember before it was released for any framework.

I’m seeing some red flags in the code you posted which may be the reason it’s not working. Either way I think you could simplify the code a little bit.

this.photos._objects
            ^ this doesn't seem right

First off this line doesn’t seem like it should need to use a private var like _objects. If this can be a sync hasMany then you can just loop over photos.albums. If it needs to be an async hasMany there are other ways to deal with this.

Secondly I don’t think you should need any of the runloop calls in your test. Nor do you need all of the get and setProperties calls (assuming you’re not using Ember < 3.20 or whatever).

  hooks.beforeEach(function () {
    const store = this.owner.lookup('service:store');
    album = store.createRecord('PhotoAlbum');
    photos = [
      store.createRecord('Photo'),
      store.createRecord('Photo'),
      store.createRecord('Photo'),
    ];
  });

  test('Photo count', function (assert) {
    assert.expect(1);
    photos[0].amountOfTags = 0;
    photos[1].amountOfTags = 1;
    photos[2].amountOfTags = 5;
    album.photos = photos;
    assert.equal(album.amountOfTaggedPhotos, 2, "Amount of tags in album is correct");
  });

Third you should be careful when declaring variables like album and photos at file scope and then not reassigning them (in your original code you’re always using the same array instance for photos which makes it all too easy to leak state into other tests. The way I’ve written it above at least make sure the array is replaced each time but even better practice might be setting vars on the test context:

  hooks.beforeEach(function () {
    const store = this.owner.lookup('service:store');
    this.photos = [
      store.createRecord('Photo'),
      store.createRecord('Photo'),
      store.createRecord('Photo'),
    ];
    this.album = store.createRecord('PhotoAlbum', { photos });
  });

  test('Photo count', function (assert) {
    assert.expect(1);
    photos[0].amountOfTags = 0;
    photos[1].amountOfTags = 1;
    photos[2].amountOfTags = 5;
    assert.equal(album.amountOfTaggedPhotos, 2, "Amount of tags in album is correct");
  });

I think making some of these changes might help you avoid some common pitfalls and also make your tests a little easier to write. As for why this code isn’t working in your tests I’d suggest using a better way of accessing the hasMany data (not using _objects in your getter). For example you could try (might need to use get if this is an async hasMany):

    this.get('photos').forEach((photo) => {
      counter += photo.amountOfTags > 0 ? 1 : 0;
    })

or maybe:

    this.photos.content.forEach((photo) => {
      counter += photo.amountOfTags > 0 ? 1 : 0;
    })

again if this can be a sync hasMany you don’t have to worry as much because you don’t have to deal with the async behavior

Thank you soo much for the quick response, using this fixed the issue:

    this.get('photos').forEach((photo) => {
      counter += photo.amountOfTags > 0 ? 1 : 0;
    })
1 Like