24 Feb 2016 · Software Engineering

    Test-Driving Ember.js Components

    14 min read
    Contents

    In the last tutorial in this series, we created an API using Rails 5 (which just went to Beta 1). In that tutorial, we mostly focused on the server, but we also considered what we were going to do with the Ember application we built in the first tutorial in the series.

    We recommend that you check out both tutorials if this is the first one you happened to find. You can also take a look at the GitHub repo with the source code. It has been tagged with “Part1End” to allow you to revert back to Part 1 and start this tutorial from there. As we go through and do some manual testing, you’re going to need the API, which is also available on GitHub. There is also a tag called “Part2End”, which is where we’ll pick up from if we tweak the API. This tutorial is primary going to be Ember.js focused.

    Upgrading Ember-CLI, Ember and Ember-Data

    Since the first tutorial, a new version of Ember-CLI was released. Let’s take a moment and upgrade Ember-CLI to the latest available version. At this time we can upgrade to 2.2.0-beta.3, though by the time you read this, 2.2.0 will probably be released. The steps for upgrading are listed with each release, but we’ll walk you through them here.

    The first step is to clean up the globally installed Ember-CLI. We do this by running the following commands:

    1. npm uninstall -g ember-cli
    2. npm cache clean
    3. bower cache clean
    4. npm install -g ember-cli@2.2.0-beta.3

    We uninstalled our current version of Ember-CLI, cleared out NPM and Bower’s cache, and installed the latest version of Ember-CLI.

    Next, we need to update our project. This is where things can go awry, because we’ll be making changes directly to the project, and it’s often easy to accidentally overwrite one of your files because you didn’t review a change, or you missed a change. We’ll be using the diff option a lot. To start, move into the bookcase application directory (the Ember.js project directory). Here are the steps to upgrade the project:

    1. rm -rf node_modules bower_components dist tmp
    2. npm install --save-dev ember-cli@2.2.0-beta.3
    3. npm install
    4. bower install

    Now, let’s move on to ember init. We’ll go file by file to make sure we don’t make any mistakes. By the way, this is a good reason to have source control. While we are going through these changes, you can always diff your files using the command d to see what will be modified.

    1. Say yes (y) to Overwrite .watchmanconfig?
    2. Say yes to Overwrite app/app.js?
    3. Say yes to Overwrite app/index.html?
    4. Say yes to Overwrite app/router.js?
    5. Say yes to Overwrite bower.json?
    6. Say yes to Overwrite ember-cli-build.js?
    7. Diff (d) Overwrite package.json?

    You’ll see we can’t just overwrite it as we have ember-validations specified, and that would get deleted. So, what we’ll do is change our file to match the diff. In the end, your devDependencies should look like the following:

    "devDependencies": {
      "broccoli-asset-rev": "^2.2.0",
      "ember-ajax": "0.7.1",
      "ember-cli": "2.2.0-beta.3",
      "ember-cli-app-version": "^1.0.0",
      "ember-cli-babel": "^5.1.5",
      "ember-cli-dependency-checker": "^1.2.0",
      "ember-cli-htmlbars": "^1.0.1",
      "ember-cli-htmlbars-inline-precompile": "^0.3.1",
      "ember-cli-inject-live-reload": "^1.3.1",
      "ember-cli-qunit": "^1.1.0",
      "ember-cli-release": "0.2.8",
      "ember-cli-sri": "^2.0.0",
      "ember-cli-uglify": "^1.2.0",
      "ember-data": "^2.2.1",
      "ember-disable-proxy-controllers": "^1.0.1",
      "ember-export-application-global": "^1.0.4",
      "ember-resolver": "^2.0.3",
      "ember-validations": "2.0.0-alpha.4"
    }

    If you save the file and then diff again, you’ll see that we’ve fulfilled all the requirements, and it would delete our ember-validations, so say no (n) to Overwrite package.json?:

    1. Say yes to Overwrite tests/helpers/resolver.js?
    2. Say yes to Overwrite tests/helpers/start-app.js?
    3. Say yes to Overwrite tests/index.html?

    Most of our answers are yes because we’ve only scratched the surface with the application by creating models. Let NPM and Bower install our dependencies. We have a conflict with Bower.

    Installing browser packages via Bower...
      conflict Unable to find suitable version for qunit-notifications
        1) qunit-notifications ~0.0.6
        2) qunit-notifications ~0.1.0
    [?] Answer:

    Choose 2 – that’s the version that we want.

    Now, for a smoke test. Let’s run our tests. Type the following into the terminal: ember test. They all pass. You can also execute the server using the command ember serve, then connect your browser to http://localhost:4200, and you’ll be greeted by a page with “Welcome to Ember.”

    What exactly did we do when we upgraded Ember-CLI? Well, first and foremost, Ember-CLI was upgraded to 2.2.0.beta.3. With that came some other updates. If you look at bower.json, you’ll notice that Ember.js is now at v2.2.0 and Ember-Data is set to ^2.2.1. If you are wondering what the caret before the 2.2.1 means, take a look at this StackOverflow answer. In addition, many of the other required packages have been upgraded.

    Acceptance Testing Our Book List

    Now that we’re all upgraded, let’s start testing. We’ve already used unit tests to test our models. Now, let’s make use of them. Instead of beginning with a unit test, we’ll work with an acceptance test. We’ll be working on the book list, so we’ll call our test something along those lines. Here’s a place Ember-CLI can help us out.

    ember g acceptance-test book-list

    This command simply creates a single file, tests/acceptance/book-list-test.js. Open that file, and let’s get started testing. But first, let’s spin up the Ember test server to watch our tests in real time: ember test --serve. This is handy if you have a second monitor. It seems that we have a failing test already. A quick look at the file will show us that Ember-CLI stubbed out a simple test for us. Let’s tweak that test. The path to our books will simply be books, not book-list like the stubbed out test. Go ahead and change that path and description. The test will still be failing because we don’t have any routes or anything else set up.

    Assertion Failed: The URL '/books' did not match any routes in your application

    Let’s fix that problem. Open up app/router.js, and add a route for books. Your Router.map section should look like this:

    Router.map(function() {
      this.route('books');
    });

    With that route in place, we’re back to green and we can move onto our next test. This one will prove that books are actually showing on the screen. But, hang on a second, how are we going to get a collection of books into our test?

    Using ember-data-factory-guy

    The answer is a package by Daniel Sudol called ember-data-factory-guy. With this package, we can just use standard Ember-Data methods (like findAll) to grab records, and ember-data-factory-guy will intercept our requests transparently and allow us to use our mock records instead of a live connection. It’s an incredible asset to add to your testing library. Let’s quickly install ember-data-factory-guy. It’s an Ember-CLI add-on, so it’s easy to install. Just run the following command in your terminal:

    ember install ember-data-factory-guy

    Now, we can go ahead and write our next test. We want to create a collection of books and assert that they exist on the page. First, we will build a factory so we can create mock books. An ember-data-factory-guy factory is simple to set up. Create a new directory called factories under the tests directory. Then, create a new file named books.js in that factories directory. Here’s the code:

    import FactoryGuy from 'ember-data-factory-guy';
    
    FactoryGuy.define('book', {
      sequences: {
        bookTitle: function(num) {
          return 'Book ' + num;
        }
      },
    
      default: {
        title: FactoryGuy.generate('bookTitle'),
        isbn: '0123456789',
        cover: 'http://placehold.it/150x200'
      }
    });

    This should be pretty straight-forward, with the exception of the sequences. Ember-data-factory-guy allows us to generate a sequence so that we don’t have properties that are the same when creating mocks. We set the book titles to be “Book 1”, “Book 2”, etc. so that we wouldn’t see the same title. If you prefer, you can do a similar thing for the ISBN.

    Before continuing, restart your test server. We need to do this because we installed an add-on, and we want to avoid getting an error when trying to import ember-data-factory-guy.

    With our factory defined, we can now create some models on the fly, and test the results. Before we do that, we need to spin up (and tear down) ember-data-factory-guy’s TestHelper. We’ll do this inside Ember-CLI’s new tests/helpers/module-for-acceptance.js file. This will load before our acceptance tests.

    ...
    import TestHelper from 'ember-data-factory-guy/factory-guy-test-helper';
    
    export default function(name, options = {}) {
      module(name, {
        beforeEach() {
          this.application = startApp();
          TestHelper.setup();
          ...
        },
    
        afterEach() {
          TestHelper.teardown();
          destroyApp(this.application);
          ...
        }
      });
    }

    The code has trimmed down the code just to show the changes. First, we import the TestHelper. Then, we call TestHelper.setup(); after the application is started. Finally, we call TestHelper.teardown(); before the application is destroyed. With that in place, we’ll turn our attention back to our test.

    Back to Testing

    Back in tests/acceptance/book-list-test.js, we’ll start by importing the TestHelper again, as we’ll be using it to mock our data store. Then, we can move on the the test. First, we’ll tell ember-data-factory-guy’s TestHelper that we want it to handle calls to find all (the Ember-Data method we’ll be using is called findAll), and return three book models, or factories. Then, we’ll visit the page. Finally, we’ll count the number of HTML items with a class of book on the page, and assert that the result should be three.

    ...
    import TestHelper from 'ember-data-factory-guy/factory-guy-test-helper';
    
    ...
    
    test('should show list of books', function(assert) {
      TestHelper.handleFindAll('book', 3);
      visit('/books');
    
      andThen(function() {
        assert.equal(find('.book').length, 3);
      });
    });

    If everything is right and you have the Ember Test Server running (ember test --serve), you should have a failing test.

    There are two steps to fixing this, one is to load the models (the books), and then render those books on the screen. Head over to app/routes, and we’ll add a new route file named books. The name of route file matches the name of the route we created earlier in app/router.js, which was also called books. You could have also used the Ember-CLI generators to create the file for you, but this isn’t required.

    In the new books.js route file, we’ll use the model hook of the route to load our data. With Ember-Data, it’s easy to find all our books.

    import Ember from 'ember';
    
    export default Ember.Route.extend({
      model: function() {
        return this.store.findAll('book');
      }
    });

    Now, the final step is to do something with the models. Here’s another convention: we’re going to add a template for the books, so we’re going to add a file named books.hbs to app/templates. In that file, we’ll just make our test pass:

     {{#each model as |book|}}
      <div class="book">{{book.title}}</div>
    {{/each}}

    Assuming the Ember test server is running, when you save you will find that you have a problem with Acceptance | book list: visiting /books. The error is Error: Adapter operation failed. Let’s consider what the problem could be. It’s the fact that we now have a call to this.store.findAll('book') in our route, but the adapter doesn’t know what to do with the request because we’re in a test. It tries to just query the URL /books on our current “server” (http://localhost:7357/books), and there is nothing there. The fix is simple, we need that test to handle the findAll call like we just did with the latest test. In this case we don’t need any models. We just need the call to be intercepted in order to pass zero to the handleFindAll call.

    test('visiting /books', function(assert) {
      TestHelper.handleFindAll('book', 0);
      visit('/books');
      ...
    });

    With that change, all our tests pass again.

    Turning the Book List Into a Component

    Let’s add interactive filtering and sorting ability to our book list. To accomplish this, we’re going to create a component for the book list. Ember-CLI can generate a component, template for component and an integration test for us, so let’s use that:

    ember g component book-list

    Since we’re test-driving this application, we’ll start with the integration test (tests/integration/components/book-list-test.js). Delete the it renders test. We’ll start with testing that it renders a list of books, just like we did with our template in the last section. We need to do a little setup to use ember-data-factory-guy in our integration tests. Unfortunately, we have to boot the application like an acceptance test for now. There is an open issue on GitHub regarding this. In the meantime, we’ll just start the app up. Here is the full file for our first test:

    import { moduleForComponent, test } from 'ember-qunit';
    import hbs from 'htmlbars-inline-precompile';
    import FactoryGuy from 'ember-data-factory-guy';
    import Ember from 'ember';
    import startApp from '../../helpers/start-app';
    
    var App = null;
    
    moduleForComponent('book-list', 'Integration | Component | book list', {
      integration: true,
    
      setup: function () {
        Ember.run(function () {
          App = startApp();
        });
      },
    
      teardown: function () {
        Ember.run(App,'destroy');
      }
    });
    
    test('it renders a list of books', function(assert) {
      this.set('books', FactoryGuy.makeList('book', 2));
      this.render(hbs{{book-list books=books}});
    
      assert.equal(this.$('.book').length, 2);
    });

    You’ll notice that we’re importing startApp from the helpers directory (import startApp from '../../helpers/start-app';). This gives us the ability to start up the full application in our setup function. Remember, this could be going away for integration tests + ember-data-factory-guy. In the teardown function, we destroy the App. These are things that are done for us when writing acceptance tests via module-for-acceptance helper. With that out of the way, let’s look at the test itself. We’re testing whether our component can render a list of books, as stated before. In the first line, we set the books property of the component that we are testing to a list of books created by FactoryGuy. In the next line, we render the component like we would on the page, with the books property set to the books collection. This syntax is a little weird since we’re rendering handlebars templates in code. Finally, we make sure that two items with a book class are rendered on the screen from the template provided.

    With the test out of the way, we’ll move to the code. As of right now, we don’t actually need to touch app/components/book-list.js, though it’s a good practice to put in the properties that you’re expecting at the top of the file:

    import Ember from 'ember';
    
    export default Ember.Component.extend({
      books: []
    });

    We could have set it to null, but an empty array could be a better option. Now, we’ll move to the template (app/templates/components/book-list.hbs). This is going to look a whole lot like the template we wrote before, with a minor change:

    {{#each books as |book|}}
      <div class="book">{{book.title}}</div>
    {{/each}}

    Instead of {{#each model as |book|}}, we’re now using the books property on the component. With that change in place, your tests should be back to green.

    One last step before we improve our component – we’ll use it in our books.hbs template (app/templates/books.hbs) instead of what we have in there now. This changes that template to just a single line:

    {{book-list books=model}}

    Our template is now nice and clean. This design will help us when routable components become a thing in Ember. We’ll be able to get rid of app/templates/books.hbs and just use our component’s template and code.

    Conclusion

    We accomplished quite a bit in this tutorial. We upgraded Ember-CLI, Ember, Ember-Data, and associated packages. We leveled up our testing using ember-data-factory-guy. We worked with templates, then converted our simple template into an Ember component. Now that we have our book-list in a component, we can start adding some neat functionality to it. In the next tutorial in the series, we’ll do just that.

    If you followed along with the tutorial, you probably don’t need it, but you can grab the source code for this tutorial on GitHub. The code is tagged with “Part3End,” which makes it easy to see what we did between Part 1 and Part 3.

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    Avatar
    Writen by:
    I am a software architect with over 18 years of experience. I simply love coding! I have a driving passion for computers and software development, and a thirst for knowledge that just cannot be quenched.