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

component testing #106

Open
catmando opened this issue Dec 20, 2015 · 5 comments
Open

component testing #106

catmando opened this issue Dec 20, 2015 · 5 comments

Comments

@catmando
Copy link
Collaborator

Need some way to test components.

Right now no reason why capybara will not work, but that is for testing entire view (or component tree), but we should have a way to "unit test" individual components.

What we need is a way to test an individual component through a set of lifecycle changes (i.e. new params, or events) and then see what the final result is.

Like this

    MyComponent.test do |c|  
      c.mount(...)           # mount component and send parameters
      c.send_event(...)  # send event...
      c.wait(...)              # simulate elapsed time
      c.update(...)         # send new parameters
    end.will_render do 
     div { SomeAppComponent(p1: ... p2 ...) } }  
    end

How..???

test should be straight forward - it makes a dummy component that responds to mount, send_event, etc. Each of these just updates state in the dummy component / uses react event test harness, to rerender MyComponent.... The whole thing returns an object that responds to will_render, which will just dump out the dummy_components DOM.

Meanwhile some monkey patching needed so that all Application components just render "themselves" (i.e. SomeAppComponent(p1: ... p2: ...) renders <SomeAppComponent params: "...">

Finally will_render is set up so that it renders the block to a static string, such that the string should be an exact match.

@ajjahn
Copy link
Collaborator

ajjahn commented Jan 8, 2016

@catmando Yeah, I agree. We need to provide some testing tools.

I began adding test matchers here just to clean up our own tests.

That matcher has the syntax:

expect(MyComponent).to render('<div>Hi</div>').with_params(say: 'Hi')

Currently the render matcher only takes a string, but we could allow it to take other components, react DSL elements, regex, etc as your will_render suggestion would do.

I think we could implement some test helpers that follow a mixture of conventions from rspec mocks and capybara. Something like:

mount(MyComponent).with_params(say: 'Hi') # Like capybara's visit '/some/url'
# now we have a mounted component object in the current context like capybara's page object

component.update_params(say: 'Bye')
# or infer the receiver:
update_params(say: 'Bye')

send_event(:some_event, 'some arg')
wait(10)

expect(component).to render('<div>Bye</div>')
# or
expect(component).to render(div { 'Bye' })

Expect the component under test to render sub components (mock like):

expect(SubComponent).to mount_with_params(foo: 'bar')
mount(MyComponent).with_params(say: 'Hi')

Or spy like syntax:

mount(MyComponent).with_params(say: 'Hi')
expect(SubComponent).to_have mounted_with_params(foo: 'bar')

One thing I like about this way of doing things, is we don't need to add anything to the internals of the component class itself and you can break the test apart a bit. For example, I could mount a component in a before block:

describe MyComponent do
  before { mount(MyComponent).with_params(...) }
  it 'looks rad' do
    expect(component).to render(...)
  end
  ...
end

Thoughts?

@catmando
Copy link
Collaborator Author

catmando commented Jan 8, 2016

That all looks good.

In particular I really like this:

mount(MyComponent).with_params(say: 'Hi') # Like capybara's visit '/some/url'
# now we have a mounted component object in the current context like capybara's page object
... etc...

If we could make that work right with capybara that would quite nice I think...

In terms of mocking subcomponents....

I am thinking that the default behavior is to "Mock" all application sub components as suggested in the original post, with some way to override this and generate the real component.

In the meantime I just got done going through are (limited) integration tests, and its surprisingly easy to use capybara-webkit for testing components with state changes.

And for stateless components (i.e. no client side JS execution) straight capybara works fine.

@catmando
Copy link
Collaborator Author

Okay, I did an initial implementation for testing our site, and here is what I have come up with (just the mount so far)

  1. because I want to support driving this from the server side (more on this below) the component name is a string (not a constant.)

  2. mount("MyComponent").with_params won't work since you can't tell when to mount the component (as far as I can see). So I have put the params in the mount method as a hash. Not sure this is a bad thing anyway since you always mount with params (possibly empty) so they really go together.

    mount("MyComponent", param1: :foo, param2: :bar)

    The alternative might be to say `mount("MyComponent").with_params(..).render.

  3. To control prerendering and other options that I found useful, a second set of options can also be included:

    mount("MyComponent", {... params ...}, {...opts...})
    # options: any options that can be passed to controller render (i.e.layout)
    # plus: 
    #  :render_on => :server_only | :client_only | :both(default)
    #  :style_sheet => "style file to include" defaults to 'application'
    #  :javascript => "js file to include" defaults to 'application'
    #  :controller => MyTestController (more on this below)

    Given all this the above proposal to use .render to trigger the actual rendering might work nicely. All the above options would be provided to the render method...

With this in place I was able to put together our first component test case (for our tweet card display)

require 'spec_helper'

describe 'tweet component', :js => true do

  before(:each) do
    size_window(:small, :portrait) # use approx size that final card will fit into
    @tweet = FactoryGirl.build(:tweets).sample
    # individual tweets have no dynamic behavior, so we just test with prerendering
    mount "Tweet", {tweet: @tweet}, {render_on: :server_only}
  end

  it "is a tweet-card" do
    page.should have_css('div.tweet-card')
  end

  it "has the tweeter's img" do
    page.find('img.profile-image')['src'].should eq(@tweet[:user][:profile_image_url_https])
  end

end

Discoveries:

  1. Much more convenient to just run server side. You can run with selenium for initial debug and see your stuff displayed. Then switch to poltergeist for continuous integration. Everything just runs as part of your main test, no separate directories, or setup. Also you can use methods like save_screenshots to document test results.... really handy.

  2. Poltergeist is much better than capybara-webkit, which kept crashing and doing weird stuff. Poltergeist has been flawless.

  3. But you do need to load the application JS (for client side rendering) and style sheets (if you want to see how stuff looks.) So the mount mechanism by default will load the application js, and css by default but that can be overridden (or set to nil for none.)

  4. And of course you need a controller, so the mount mechanism sets up a controller that is subclassed off of ActionController::Base that by default is called ReactTest, and route called react_test. The controller has one method called test, which does all the monkey business.

  5. You do not by default want to subclass off of ApplicationController since of course it might not even be there, or in our case it has a lot of before hooks to do with domain matching etc, that get in the way of testing your component. However in some cases you might need to either change the name of the controller/route or actually subclass off of ApplicationController, so that is why I added the controller option. Here you would build an empty controller class in your test, and pass it to mount which will add the appropriate route, and the test method.

    it "can mount a tweet in the application layout" do
      Object.const_set("MyTestController", Class.new(ApplicationController))
      FactoryGirl.create :production_center, domain: "127.0.0.1", code: "US"
      mount "Tweet", {tweet: @tweet}, {controller: MyTestController}
      page.evaluate_script('window.location.pathname').should start_with("/my_test/")
      page.should have_css('div.tweet-card')
    end

Non Rails Environments

Not sure about this, but feel that its just like above, but easier.

Other features

I think we this basic mount in place sending new parameters, and events should be straight forward.

To mock the subcomponents will require some support inside of react.rb, but I think straight forward as well. I will hopefully be able to test by monkey patching.

Prerender = Client

It would be very nice to have an option on mount that raises an error if the prerendered component != the client rendered component.

@wied03
Copy link
Contributor

wied03 commented Mar 25, 2016

I love doing capybara testing but I think that's no substitute for unit testing on the JS side.

I wrote a shared RSpec context that uses React.render, React.addons.TestUtils.renderIntoDocument, and hooks into the lifecycle for mounts/updates to know when it can assert, then I use a combination of findDOMNode and other React test utils for simulating clicks, etc.

I'm not sure I'm ready to throw it out here yet but I've had good luck with it. I don't have to modify my components to test them either.

@sollycatprint
Copy link

This issue was moved to ruby-hyperloop/hyper-react#106

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

No branches or pull requests

4 participants