[Code Review] Getting Started with Testing

I’m just getting started with testing and I’m learning. I wrote a small component that truncates and expands text and wrote some tests for it. I’d appreciate any feedback to help me improve. The only change I was thinking of making was using Fakerjs to generate the lorem ipsum on the fly instead of hard coding it. Thanks!

// app/components/util/truncate/component.js
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';

export default class TruncateComponent extends Component {
	@tracked readMore = false;


	get trimmedContent() {
		return this.args.content.substring(0, this.length);
	}

	/**
	 * Checks if content needs to be trimmed
	 * @return {boolean} trimmed
	 */
	get contentTrimmed() {
		return this.args.content.length > this.length;
	}

	/**
	 * Gets the length of the content
	 * @return {number} length
	 */
	get length() {
		return this.args.length || 200;
	}

	get showReadMore() {
		if('readMore' in this.args && !this.args.readMore) {
			return false;
		}

		if(this.args.content.length < this.length) {
			return false;
		}

		return true;
	}

	@action
	toggleReadMore(evt) {
		evt.preventDefault();
		this.readMore = this.readMore ? false : true;
	}
  }
// app/components/util/truncate/template.hbs
{{#if this.readMore}}
  {{@content}} 
{{else}}
  {{this.trimmedContent}}{{if this.contentTrimmed '…' ''}}
{{/if}}

{{#if this.showReadMore}}
  <a {{on "click" (fn this.toggleReadMore)}} href="#">
    {{#if this.readMore}}
      Read Less
    {{else}}
      Read More
    {{/if}}
  </a>
{{/if}}
   // tests/integration/component/util/truncate/component-test.js
    import { module, test } from 'qunit';
    import { setupRenderingTest } from 'ember-qunit';
    import { click, render } from '@ember/test-helpers';
    import { hbs } from 'ember-cli-htmlbars';

    module('Integration | Component | util/truncate', function(hooks) {
      setupRenderingTest(hooks);

      test('it renders', async function(assert) {
        // Set any properties with this.set('myProperty', 'value');
        // Handle any actions with this.set('myAction', function(val) { ... });

        await render(hbs`<Util::Truncate @content="Sample text" />`);

        assert.equal(this.element.textContent.trim(), 'Sample text');

      });

      test('it truncates long text', async function(assert) {
        var lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque neque metus, condimentum at ipsum a, feugiat ullamcorper enim. Nulla finibus at nulla ac faucibus. Fusce sodales id diam eget fermentum. Fusce auctor, diam maximus imperdiet interdum, risus mauris volutpat tortor, a aliquam urna velit ac nisi. Morbi eget congue justo. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Suspendisse a rhoncus felis."
        var includes = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque neque metus, condimentum at ipsum a, feugiat ullamcorper enim. Nulla finibus at nulla ac faucibus. Fusce sodales id diam eget ferm…';
        this.set('lorem', lorem);

        await render(hbs`<Util::Truncate @content="{{this.lorem}}" />`);

        assert.equal(this.element.textContent.includes(includes), true);
      });

      test('it shows short text', async function(assert) {
        var lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque neque metus, condimentum at ipsum a, feugiat ullamcorper enim."

        this.set('lorem', lorem);

        await render(hbs`<Util::Truncate @content="{{this.lorem}}" />`);

        assert.equal(this.element.textContent.trim(), lorem);
      });

      test('it toggles text', async function(assert) {
        var lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque neque metus, condimentum at ipsum a, feugiat ullamcorper enim. Nulla finibus at nulla ac faucibus. Fusce sodales id diam eget fermentum. Fusce auctor, diam maximus imperdiet interdum, risus mauris volutpat tortor, a aliquam urna velit ac nisi. Morbi eget congue justo. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Suspendisse a rhoncus felis."
        var includes = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque neque metus, condimentum at ipsum a, feugiat ullamcorper enim. Nulla finibus at nulla ac faucibus. Fusce sodales id diam eget ferm…';
        this.set('lorem', lorem);

        await render(hbs`<Util::Truncate @content="{{this.lorem}}" />`);

        assert.equal(this.element.textContent.includes(includes), true);

        await click('a');

        assert.equal(this.element.textContent.includes(lorem), true);

        await click('a');

        assert.equal(this.element.textContent.includes(includes), true);

      });

      test('it allows custom length', async function(assert) {
        var lorem = "Lorem ipsum dolor sit amet."
        var includes = 'Lorem…';
        
        this.set('lorem', lorem);

        await render(hbs`<Util::Truncate @content="{{this.lorem}}" @length={{5}} />`);

        assert.equal(this.element.textContent.includes(includes), true);
      });

      test('it hides read more', async function(assert) {
        var lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque neque metus, condimentum at ipsum a, feugiat ullamcorper enim. Nulla finibus at nulla ac faucibus. Fusce sodales id diam eget fermentum. Fusce auctor, diam maximus imperdiet interdum, risus mauris volutpat tortor, a aliquam urna velit ac nisi. Morbi eget congue justo. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Suspendisse a rhoncus felis."

        this.set('lorem', lorem);
        await render(hbs`<Util::Truncate @content="{{this.lorem}}" @readMore={{false}} />`);

        assert.equal(this.element.textContent.includes('Read More'), false);
      });


    });

Thanks for asking for help! I think you understood how to write a component and what to test well. I also liked seeing how you added an empty line to separate groups of related code.

I have some suggestions for improving the maintainability of your code. Assuming your tests pass right now, let’s start with updating the tests before updating the component:

  1. I prefer to always use assert.strictEqual, which is analogous to === in JavaScript. I’d also change var to const or let. It looks like you’re using a recent version of Ember. If so, you don’t need to use this.set.
this.content = 'lorem';

await render(hbs`
  <Util::Truncate
    @content={{this.content}}
    @readMore={{false}}
  />
`);
  1. As you may have felt, this.element.textContent.trim() is a chore to type and makes your test hard to read (for other people on your team as well as your future self). Use QUnit DOM to write readable assertions: :100:
// Before
await render(hbs`<Util::Truncate @content="Sample text" />`);

assert.equal(this.element.textContent.trim(), 'Sample text');

// After
await render(hbs`
  <Util::Truncate
    @content="Sample text"
  />
`);

assert.dom(this.element)
  .hasText('Sample text', 'We see the full text.');
  1. As you likely noticed, you had to use this.element.textContent.includes() because the component renders many things but you want to test just 1 thing at a time. If you haven’t yet, I recommend installing ember-test-selectors. This addon lets you mark DOM elements that you are interested in testing.
<span data-test-content>
  ...
</span>

<button data-test-button="Toggle" type="button">
  ...
</button>
assert.dom('[data-test-content]')
  .hasText('Lorem ipsum <omitted> diam eget ferm…', 'We see the trimmed text.');

assert.dom('[data-test-button="Toggle"]')
  .hasText('Read More', 'The toggle button shows Read More.');

await click('[data-test-button="Toggle"]');

assert.dom('[data-test-content]')
  .hasText('Lorem ipsum <omitted> a rhoncus felis.', 'We see the full text.');

assert.dom('[data-test-button="Toggle"]')
  .hasText('Read Less', 'The toggle button shows Read Less.');
1 Like

Next, let’s look at updating the component.

  1. I think the name of tracked variable readMore is misleading, given how it is used in the template. A better name may be showFullText? Similarly, I’d rename the getter and argument length to maxLength.

  2. I think it’s good to use <button> instead of <a> for toggling. If you go with the button, you can remove event.preventDefault(); in your action.

<button
  data-test-button="Toggle"
  type="button"
  {{on "click" this.toggleReadMore}}
>
  ...
</button>
@action toggleReadMore() {
  this.readMore = !this.readMore;
}

After you make these changes, run tests again to make sure that your component works just as before.

1 Like

Finally, regarding your question about using Faker, in general, for web apps, it’s good to not randomize the test input. This way, you can be confident that, when a test fails, it’s due to a recent change in your code and not due to starting out with different assumptions. If you also start to use Percy or Backstop for visual regression tests, you will want to see the same data captured. (If you do want to randomize, set a seed.)

Whether random input is helpful is interesting to think about. When I tested math libraries that my students had created, testing their code against random inputs and comparing the outputs to Matlab’s helped a lot with grading their code fast. :smirk:

If you want to learn more about how to write tests, I recommend reading these articles:

1 Like