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

feat(priceRanges): Add BEM classes and tests #390

Merged
merged 1 commit into from
Nov 2, 2015
Merged

Conversation

pixelastic
Copy link
Contributor

Fixes #387

BREAKING CHANGE: ais-price-ranges--range are now named
ais-price-ranges--item and are wrapped in
a ais-price-ranges--list.

I've moved the bottom form into it's own PriceRangesForm component,
along with its own tests. I've fixed a minor typo where the component
was internally named PriceRange (without the final s).

I factorize some logic form the render in individual methods and
manage to individually test them. This was not an easy task. I had to
mock the default render (so it does nothing) before instanciating
the component. Then, I was able to call each inner method
individually. This requires to stub prototype methods in beforeEach,
then restore them in afterEach. I've added a few helper methods, this
can surely be simplified again but this gives nice granularity in
testing.

I've renamed the range items to item and wrapped them in a list.
I've also added classes to all elements we add (label, separator,
etc). I've removed the empty spans.

{fromInput}
<span className={this.props.cssClasses.separator}> {this.props.labels.separator} </span>
{toInput}
<button className={this.props.cssClasses.button} type="submit">{this.props.labels.button}</button>
Copy link
Contributor

Choose a reason for hiding this comment

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

The only issue with that code is that there won't be any space between the toInput and the button, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right, but why do you need one? This should be handled by CSS

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm happy with that; it's the same for the checkboxes in the refinementList widget anyway.

@redox
Copy link
Contributor

redox commented Oct 30, 2015

Are you able to rebase so we can merge?

@pixelastic
Copy link
Contributor Author

On it

return this.getItemFromFacetValue(facetValue);
})}
</div>
{form}
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not directly call the <Form> instead of calling a function? Not sure this is a common pattern in React community.

The <Form> call is only 5 LOC and it would be ovious to read the render() then?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The goal was to be able to:

  • Unit test the getForm method separatly
  • Mock the getForm when testing render

This lets me focus on one thing in each test. In the end I think I did not unit test getForm (my bad, I might have forgotten).

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'll add the missing test and rebase

Copy link
Contributor

Choose a reason for hiding this comment

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

The goal was to be able to:

Unit test the getForm method separatly
Mock the getForm when testing render
This lets me focus on one thing in each test. In the end I think I did not unit test getForm (my bad, I might have forgotten).

I think we should try to avoid mocking when we can, you can always store the expected <Form> in a variable in your test rather than modifying the implementation to make it easily testable.

The least mocking we have in testing render() the better?

Copy link
Contributor

Choose a reason for hiding this comment

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

Small detail in the end, if you test getForm() that's ok

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 maybe due to the fact that most components we always only test the render() method even if that's some boilerplate to add. This way we can just refactor the whole component really easily.

In the end testing a React component output is just asserting some JSX on render(), the underlying implementation should not have to be tested when we can avoid it (sometimes for handlers we need to test it).

@vvo
Copy link
Contributor

vvo commented Nov 2, 2015

really really awesome :D

@pixelastic
Copy link
Contributor Author

it('starts a refine on submit', () => {
// Given
let refine = sinon.spy();
let handleSubmitMock = sinon.spy(PriceRangesForm.prototype, 'handleSubmit');
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not test the instance method rather than the prototype:

let component = <PriceRangesForm refine={refine} />;
TestUtils.renderIntoDocument(<PriceRangesForm refine={refine} />);

If you keep the prototype, you will also need to PriceRangesForm.prototype.handleSubmit.restore();

Copy link
Contributor

Choose a reason for hiding this comment

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

Also here I think it's just a "spy" rather than a full mock. http://sinonjs.org/docs/#mocks

Found it strange to read "mock" 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.

Why not test the instance method rather than the prototype:

Well... I tried but failed. <PriceRangesForm refine={refine} /> does not have an handleSubmit method so I cannot spy it. Once "rendered into document" it does have one, but it is then too late to spy on it as it has already been called. That's why I ended up spying on the prototype.

But you're right that I should restore afterwards (I did it in the other tests, forgot about it here).

And I tend to call everything mock as a generic term (spies and stubs), but I'll rename to make it clearer.

If you have a better solution for not spying on the prototype let me know and I'll update the PR. Otherwise I'll simply fix the restore/naming.

Copy link
Contributor

Choose a reason for hiding this comment

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

If you have a better solution for not spying on the prototype let me know and I'll update the PR. Otherwise I'll simply fix the restore/naming.

The PR was merged no big deal but here it is how it was done before: https://github.com/algolia/instantsearch.js/pull/390/files#diff-40651fdb5ca1bd7385705422de4e7029L102

Well... I tried but failed. does not have an handleSubmit method so I cannot spy it. Once "rendered into document" it does have one, but it is then too late to spy on it as it has already been called. That's why I ended up spying on the prototype.

Weird 1. it does not have one, I thought we already did the same tests in other components? 2. is it really called on render? Because it should be called only on submit

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The PR was merged no big deal but here it is how it was done before:

The way we did it before was not checking that our handleSubmit method was called when submitting the form, it was checking the side effects of calling the handleSubmit. Not exactly the same thing and I'd rather test the call to the method when submitting, and testing the effect of the method independently.

is it really called on render? Because it should be called only on submit

Guess I posted too fast. Will have to check again. I remember the whole testing plumbing here was hard to implement, I might have overlooked something.

Copy link
Contributor

Choose a reason for hiding this comment

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

it was checking the side effects of calling the handleSubmit

Nope it was actually simulating changing the form and submiting it: https://github.com/algolia/instantsearch.js/pull/390/files#diff-40651fdb5ca1bd7385705422de4e7029L115

Which is the interface we want to test:

  • input: submit form
  • output: call my spy refine with the right values

Testing that an underlying prototype implementation was called is not the same, here you enforce your implementation to have a certain method versus testing the INPUT/OUTPUT you expose

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Let me start by saying I love this discussion :)

You're actually right. I think I forgot a test on handleSubmit. What I wanted to test was:

  1. Calling handleSubmit calls the passed refine props with the correct args
  2. Submitting the for calls handleSubmit with values from the inputs

I did only half the job and thus, the previous test was better. Don't hesitate to stall the merging for issues like that next time.

Fixes #387

BREAKING CHANGE: `ais-price-ranges--range` are now named
`ais-price-ranges--item` and are wrapped in
a `ais-price-ranges--list`.

I've moved the bottom form into it's own PriceRangesForm component,
along with its own tests. I've fixed a minor typo where the component
was internally named PriceRange (without the final __s__).

I factorize some logic form the render in individual methods and
manage to individually test them. This was not an easy task. I had to
mock the default `render` (so it does nothing) before instanciating
the component. Then, I was able to call each inner method
individually. This requires to stub prototype methods in beforeEach,
then restore them in afterEach. I've added a few helper methods, this
can surely be simplified again but this gives nice granularity in
testing.

I've renamed the `range` items to `item` and wrapped them in a `list`.
I've also added classes to all elements we add (`label`, `separator`,
etc). I've removed the empty `span`s.
vvo pushed a commit that referenced this pull request Nov 2, 2015
feat(priceRanges): Add BEM classes and tests
@vvo vvo merged commit 84dca16 into develop Nov 2, 2015
@vvo vvo deleted the fix/price-ranges-bem branch November 2, 2015 10:55
@vvo vvo removed the in progress label Nov 2, 2015

// Then
expect(handleSubmitMock.calledOnce).toBe(true);
expect(refine.calledWith(10, 20)).toBe(true);
Copy link
Contributor

Choose a reason for hiding this comment

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

Actually you are testing both the input output and your internal implementation here. I would get rid of the check on handleSubmit because there's no need of it. It's not a mock since the behavior is not changed. You are just checking that your code follow an internal path you specified in your test but there's no added value versus checking (refine.calledWith)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep, I now see what you mean. I was convinced I did two tests: one for checking that handleSubmit was called on submit, and another to test that handleSubmit correctly calls refine. I did not and tested everything in the same test, my bad

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

priceRanges: Making BEM classes more consistent
3 participants