Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added unit test for select-dropdown component #9150

Merged
merged 1 commit into from
Nov 9, 2016
Merged

Added unit test for select-dropdown component #9150

merged 1 commit into from
Nov 9, 2016

Conversation

brunoscopelliti
Copy link
Contributor

Hi there,

I'm adding some unit tests for the select-dropdown component.

@gwwar, recently, you've reviewed my pull request #8915, would you mind to give me some advices to improve these tests. Feedback from other people is appreciated as well.

ps. I tried following this guide lines as much as possible, however it seems to me that the guide lines (last change November 2015) are now behind your current practices (enzyme's shallow vs React.addons.TestUtils just to make an example).

@lancewillett lancewillett added the [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. label Nov 4, 2016
* External dependencies
*/
import React from 'react';

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extra line here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that's an old habit of mine.
I like to group dependencies on the basis of their purpose; in this case I put that extra line to separate react, from the mocking/assertion libraries.

Since you don't have this convention, to let that extra line does not make sense in this case.

import SelectDropdown from '../index';

describe( 'index', function() {
require( 'test/helpers/use-fake-dom' )();
Copy link
Contributor

@gwwar gwwar Nov 4, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless we're running into load order issues, we should be able to do:

import useFakeDom from 'test/helpers/use-fake-dom';
//...
describe( 'index', () => {
     useFakeDom();

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've verified, and it still works as expected... so it's ok for me to change.
Could you also please explain what you mean with

Unless we're running into load order issues

Did you have problems in the past, which were caused by requiring use-fake-dom at the top level?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. In some cases when we pull in these dependencies they can interact with global data and mess with each other. I don't think there's something specific here with useFakeDom(), but maybe @gwwar was trying to figure out why it was a require() in-place instead of an import at the top level. I cannot speak for her though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the clarification.

why it was a require() in-place instead of an import at the top level

There's not a particular reason; I took the tests of the bulk-select component as example for many things (#8960 (comment)).
However, since having nested dependencies is not that common, I should have been more careful there.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately one will likely be mislead by copying existing code around here. We're working on curating a list of good examples to use but for now, no harm done - that's what code review is for 😄

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is more of a Babel implementation detail at the moment, but require is synchronous while import is async. We generally only use require when importing dynamically (only in certain cases), or when we need to control the load order. For example mocking out packages when using use-mockery

const dropdownOptions = getDropdownOptions();
const dropdown = mount( <SelectDropdown options={ dropdownOptions } /> );

assert.equal( 'Statuses', dropdown.find( '.select-dropdown__label' ).text() );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally find expect to be a bit more readable but we use both styles in the codebase.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that for specific assertions expect may lead to a more readable code.
At work we use QUnit, for personal projects I usually go for tape... so the assert style comes more natural to me.

I guess this could be the occasion to get more comfortable with expect.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My preference is with expect() and I'm pretty sure I see it far more in Calypso than assert

expect( dropdown.find( '.select-dropdown__label' ).text() ).to.eql( 'Statuses' );

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess thatexpect is much more popular. Some test files use assert from Node or assert from Chai. It's all fine until there is only one of them used in a single file.

import React from 'react';

import { assert } from 'chai';
import { shallow, mount } from 'enzyme';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typically we prefer shallow to avoid the dom setup, but it looks like this component is complex enough to use mount.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I won't abuse of mount in the future.
I'll check if I can get equivalent assertions without mounting the whole component... otherwise I'll keep this one as is.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In first place shallow is much faster then mount and render.


import { assert } from 'chai';
import { shallow, mount } from 'enzyme';
import sinon from 'sinon';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also have the option of using a sinon sandbox so we can avoid having to manually restore stubs:

import { useSandbox } from 'test/helpers/use-sinon';
//...
let sandbox;
useSandbox( newSandbox => {
    sandbox = newSandbox;
});
//...
const stub = sandbox.stub( React.Component.prototype, 'setState' );

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I've noticed it during my exploration of your test base; it is essentially a mocha-aware wrapper around sinon.sandbox.create.
Are you suggesting it's better to use it even in a such simple scenario?

Also, I just noticed that for the assertions on spy/stub you never use sinon.assert.
I usually prefer sinon.assert cause it's more descriptive than chai, qunit (whatever...) when an assertion fails.
Do you see any problem?

Finally, I would ask @gziolo a question about use-sinon.
I noticed that the pattern:

let sandbox;
useSandbox( newSandbox => {
    sandbox = newSandbox;
});

is diffused through the whole testbase.

I was wondering why useSandbox does not just return the newly created sandbox; and noticed that this functionality was recently added (80727d5), and then removed (f37aa64#diff-c988aa06ee204545fc3c01efb3ffe8c6).

I think the idea was pretty cool; however the initial implementation had a flaw (and probably the reason you removed it) in that it returned an empty object when executing useSandbox. That was due to the fact that sinon.sandbox.create( config ); was called too late (when mocha triggers the before hook).

I tried a different implementation, and tests still pass.
If in the next days I prepare a pull request, do you mind to review it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ran into a similar question about the usage here. Are we storing the sandbox to keep it from being garbage-colelcted during the tests?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I've noticed it during my exploration of your test base; it is essentially a mocha-aware wrapper around sinon.sandbox.create.
Are you suggesting it's better to use it even in a such simple scenario?

useSandbox takes care of tear down for you. It just simplifies a lot when you are using stubs and mocks extensively. It's fine to restore a stub inside the single test as it's done in this PR :) I don't have a preferred method here. In some cases it's also a good idea to use sinon.test which mixes both approaches.

Also, I just noticed that for the assertions on spy/stub you never use sinon.assert.
I usually prefer sinon.assert cause it's more descriptive than chai, qunit (whatever...) when an assertion fails.

Chai wraps sinon.assert so it should have similar handling. I'm not sure I have never used sinon.assert directly. We have developers with different levels of experience with unit testing. It definitely helps to have one style of writing assertions and one place to look for examples and documentation. That's why I think it's nice that we can use sinon-chai. On the other hand I don't see anything bad with using sinon.assert directly :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was wondering why useSandbox does not just return the newly created sandbox;

I'm not quite sure now, but most likely we wanted to have one style of using test helpers. It was the only helper that was returning a value. Feel free to open PR if you have an idea how to improve our code. 👍

@gwwar
Copy link
Contributor

gwwar commented Nov 4, 2016

Thanks @brunoscopelliti This looks great! It does look like https://github.com/Automattic/wp-calypso/blob/master/docs/react-component-unit-testing.md is rather out of date! Thanks for the reminder to clean this up.

cc @alisterscott and @retrofox for any other behavior we should check for.
cc @gziolo for any other testing tips

@brunoscopelliti
Copy link
Contributor Author

@gwwar Thanks for the immediate feedback.
I'll check your points, and try to improve accordingly!

const dropdown = shallow( <SelectDropdown options={ dropdownOptions } /> );

assert.equal( 1, dropdown.find( '.select-dropdown' ).length );
assert.equal( 0, dropdown.find( '.select-dropdown.is-open' ).length );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just for fun, the expect version is expect( dropdown.find( '.select-dropdox' ) ).to.have.lengthOf( 1 );

alternatively, we could use other methods…

expect( dropdown.some( '.select-dropdown' ) ).to.be.true;

@gziolo
Copy link
Member

gziolo commented Nov 7, 2016

It does look like https://github.com/Automattic/wp-calypso/blob/master/docs/react-component-unit-testing.md is rather out of date!

Yes, it's so out of date :(

expect( dropdown.find( '.select-dropdown__options li.select-dropdown__label' ).text() ).to.eql( 'Statuses' );
expect( dropdown.find( '.select-dropdown__options li.select-dropdown__option' ) ).to.have.lengthOf( 4 );

// a separator is rendered in place of any falsy option.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment gives me a hint that it deservers its own it block.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I'll update the PR soon.

@gziolo
Copy link
Member

gziolo commented Nov 7, 2016

cc @gziolo for any other testing tips

We don't have too many component tests, because they are usually quite hard to write and maintain. Anyway, we should try to improve in this area, so thanks a lot for your great effort on this PR. Nicely done! 💯

describe( 'component rendering', function() {
it( 'should render a list with the provided options', function() {
const dropdownOptions = getDropdownOptions();
const dropdown = mount( <SelectDropdown options={ dropdownOptions } /> );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can also extract those two lines into its own utility method to reduce some code duplication.

@brunoscopelliti
Copy link
Contributor Author

Thanks for the time you've dedicated reviewing this pr, and answering my questions.
Let me know if there's anything else I can do here.

return mount( <SelectDropdown options={ dropdownOptions } /> );
}

function shallowRenderDropdown( props ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool 👍

focus: sinon.spy()
}
},
activateItem: activateItemSpy,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can inline sinon.spy calls to make it more concise here and in prepareFakeEvent.

expect( initialSelectedValue ).to.equal( 'drafts' );
} );

it( 'should return `undefined`, when there aren\'t options', function() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We usually use double quotes to avoid backslash in strings like \':

"should return `undefined`, when there aren't options"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both styles are acceptable atm, there was some discussion here:
#9037 (review)

@gziolo gziolo added [Status] Ready to Merge and removed [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. labels Nov 8, 2016
@gziolo
Copy link
Member

gziolo commented Nov 8, 2016

Looks good to me. Nice set of tests which execute really fast even when we use mount. I like it 👍

I left a couple of comments, that can be addressed before we merge. They are optional, if you have time to fix them go for it, otherwise I'm going to merge this PR as it is. Unfortunately I can't do it easily myself because you are using repository fork ;)

@gziolo gziolo added the Testing label Nov 8, 2016
Changes post code review + more tests

Linting

Extract mountDropdown and shallowRenderDropdown methods

Replace comment with test

Avoid escaping

Inlining sinon.spy call
@gziolo
Copy link
Member

gziolo commented Nov 9, 2016

@brunoscopelliti thanks for working on it. I just merged your changes 🎉

@gwwar gwwar mentioned this pull request Nov 15, 2016
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants