So there's an argument that I'm reading about that says Controller tests should be unit tests of the controllers and Request specs should be more of an integration test that involves the router, the controller, and the response. That's the philosophy in the codebase that I'm working in. Given that, what's the best way to write both tests given that I have this controller:
class Api::WineController < ApplicationController
def show
wine = Wine.find(params[:id])
render json: { id: wine.id, varietal: wine.varietal }, status: :ok
rescue ActiveRecord::RecordNotFound
render json: { error: { message: "Wine not found") } }, status: :bad_request
end
end
So my request spec is the one that looks like a typical, integration-y controller test that most people write:
require 'rails_helper'
RSpec.describe "Wine API", type: :request do
describe '#get /api/wine' do
let!(:wine) { create(:wine) }
subject { get "/api/wine_invite_beta/wine_tokens/:id", params }
let(:params) {
{
id: wine.id
}
}
context "and the wine exists" do
it "returns back a JSON response of names" do
subject
expect(JSON.parse(response.body)).to eq("id" => wine.id, "varietal" => wine.varietal)
expect(response).to have_http_status(:ok)
end
end
context "and wine does not exist" do
it "returns back a JSON response of errors" do
subject
expect(JSON.parse(response.body)).to eq("error" => { "message"
....
end
end
end
end
So the request spec uses the http methods (:get is the one used), to hit the router which then hits the controller. This seems more like an integration test.
The controller test should do without the router and just test the class and its methods like I would test the model. Is that right? Do people agree? This is how I'm writing it:
require 'rails_helper'
RSpec.describe Api::WinesController, type: :controller do
describe "#show" do
let(:controller_instance) { described_class.new }
subject { controller_instance.show }
before do
allow(controller_instance).to receive(:params) { params }
end
let(:params) { { id: 1} }
context "and wine token exists for user" do
let!(:wine) { create(:wine) }
it "calls render with the wine token data" do
expect(controller_instance).to receive(:render).with(
json: {
id: wine.id,
varietal: wine.varietal
},
status: :ok
)
subject
end
end
Is that okay? I'm merely setting an expectation that a method is called within the controller because that seems like a command. That feels wrong though. The render method seems more like a query than a command and so perhaps I should be inspecting the state of the controller after render is called. Does anyone know how to do this?
More generally, what do people think of this approach to making controller vs request specs?
Related
I am trying to unit test a component in a Vue.js and Apollo app. The component's general behavior is to query the server, load the response into the data of the Vue component. When a button is pushed run a GraphQL mutation and then update the data with the response data. Here's a stub of the component.
Vue.extend({
$apollo: {
person: {
query: gql`query($id: ID!) {
person(id: $id) {
name
id
}
}`,
fetch-policy: 'network-only',
}
},
data() {
return {
person: {},
newPersonId: null,
}
},
methods: {
getNewPerson() {
this.$apollo.mutate({
mutation: gql`mutation($person: Person!) {
savePerson(person: $Person) {
person {
id
}
}
}`,
update: (store, { data }) {
////////////////////////////
// test-what-happens-here //
////////////////////////////
}
})
}
}
});
My gut says the appropriate tests to write are
it('renders the name in correctly on load')
it('sends the correct data with the mutation')
it('does the right thing when the server responds') <--- Most Important
it('does the right thing when the server responds with an error') <--- Most Important
I've written a handful of tests but none seem to be a good pattern for testing the components round trip.
If this were an axios request I would do mock the response to send a 200 and the correct data, and assert axios request was called with the right JSON and the component looked correct, based on a stubbed response.
I cannot for the life of me figure this out with vue-apollo.
They have a decent testing section but the "simple tests" that use apollo cannot test the outcome of a request, and the graphql-tools tests don't actually leverage apollo so it misses a pretty significant amount of behavior.
How would you test the update function here in the above code?
Stumped on a couple failures and want to know if I'm understanding Mirage correctly:
1.In ember-cli-mirage, am I correct that the server response I define should reflect what my actual server is returning? For example:
this.get('/athletes', function(db, request) {
let athletes = db.athletes || [];
return {
athletes: athletes,
meta: { count: athletes.length }
}
});
I am using custom serializers and the above matches the format of my server response for a get request on this route, however, on two tests I'm getting two failures with this error: normalizeResponse must return a valid JSON API document: meta must be an object
2.Is mirage enforcing the json:api format, and is it doing so because of the way I'm setting up the tests?
For example, I have several tests that visit the above /athletes route, yet my failures occur when I use an async call like below. I would love to know the appropriate way to correctly overwrite the server response behavior, as well as why the normalizeResponse error appears in the console for 2 tests but only causes the one below to fail.
test('contact params not sent with request after clicking .showglobal', function(assert) {
assert.expect(2);
let done = assert.async();
server.createList('athlete', 10);
//perform a search, which shows all 10 athletes
visit('/athletes');
fillIn('.search-inner input', "c");
andThen(() => {
server.get('/athletes', (db, request) => {
assert.notOk(params.hasOwnProperty("contacts"));
done();
});
//get global athletes, which I thought would now be intercepted by the server.get call defined within the andThen block
click('button.showglobal');
});
});
Result:
✘ Error: Assertion Failed: normalizeResponse must return a valid JSON API document:
* meta must be an object
expected true
I tried changing my server response to a json:api format as suggested in the last example here but this looks nothing like my actual server response and causes my tests to fail since my app doesn't parse a payload with this structure. Any tips or advice must appreciated.
You are correct. Are the failures happening for the mock you've shown above? It looks to me like that would always return meta as an object, so verify the response is what you think it should be by looking in the console after the request is made.
If you'd like to see responses during a test, enter server.logging = true in your test:
test('I can view the photos', function() {
server.logging = true;
server.createList('photo', 10);
visit('/');
andThen(function() {
equal( find('img').length, 10 );
});
});
No, Mirage is agnostic about your particular backend, though it does come with some defaults. Again I would try enabling server.logging here to debug your tests.
Also, when writing asserts against the mock server, define the route handlers at the beginning of the test, as shown in the example from the docs.
I was able to get my second test to pass based on Sam's advice. My confusion was how to assert against the request params for a route that I have to visit and perform actions on. I was having to visit /athletes, click on different buttons, and each of these actions was sending separate requests (and params) to the /athletes route. That's is why I was trying to redefine the route handler within the andThen block (i.e. after I had already visited the route using the route definition in my mirage/config file).
Not in love with my solution, but the way I handled it was to move my assertion out of route handler and instead assign the value of the request to a top-level variable. That way, in my final andThen() block, I was able to assert against the last call to the /athletes route.
assert.expect(1);
//will get assigned the value of 'request' on each server call
let athletesRequest;
//override server response defined in mirage/config in order to
//capture and assert against request/response after user actions
server.get('athletes', (db, request) => {
let athletes = db.athletes || [];
athletesRequest = request;
return {
athletes: athletes,
meta: { count: athletes.length }
};
});
//sends request to /athletes
visit('/athletes');
andThen(() => {
//sends request to /athletes
fillIn('.search-inner input', "ab");
andThen(function() {
//sends (final) request to /athletes
click('button.search');
andThen(function() {
//asserts against /athletes request made on click('button.search') assert.notOk(athletesRequest.queryParams.hasOwnProperty("contact"));
});
});
});
I'm still getting console errors related to meta is not an object, but they are not preventing tests from passing. Using the server.logging = true allowed me to see that meta is indeed an object in all FakeServer responses.
Thanks again to Sam for the advice. server.logging = true and pauseTest() make acceptance tests a lot easier to troubleshoot.
Rails: 4.1.7
RSpec-rails version: 3.1.0
I am trying to write a request spec to test the create action for my BlogPost model. RSpec doesn't seem to like the data params that I am trying to pass in because I keep seeing the following error when running the test:
ActiveRecord::UnknownAttributeError:
unknown attribute: blog_post
RSpec code:
require 'rails_helper'
RSpec.describe BlogPost do
let!(:admin_user) { Fabricate(:admin_user) }
let!(:blog_post) { Fabricate(:blog_post) }
before { login(admin_user.email, admin_user.password) }
describe 'POST /admin/blog_posts' do
before do
post admin_blog_posts_path, blog_post: {
body: 'body text',
title: 'title text',
cover_image: '/assets/post.png',
summary: 'cool post bruh',
live_demo_url: 'livedemo.com',
live_demo_url_text: 'click here',
github_source: 'github.com/awesome'
}
end
it 'should redirect to the blog posts index page' do
expect(response).to redirect_to(admin_blog_posts_path)
follow_redirect!
end
end
end
There is something about using the word blog_post it doesn't seem to like. Because I tried changing it to an arbitrary word like so and the error went away:
post admin_blog_posts_path, someresource: {
title: 'title text'
}
Also I have a put request spec, which is also using blog_post and that works fine:
describe 'PUT /admin/blog_posts/:id' do
before do
put admin_blog_post_path(blog_post.id), blog_post: {
title: 'My new title'
}
end
...
end
So I'm not really sure why RSpec doesn't like my post admin_blog_posts_path, blog_post ... syntax. Any ideas?
Noob status over here.
In my controller create action I had:
#blog_post = BlogPost.new(permitted_params)
It was raising the error because I passed in the entire params so it read blog_post as an attribute for the BlogPost resource.
The fix: #blog_post = BlogPost.new(permitted_params[:blog_post])
I have a controller I'm testing with Ember CLI, but the controller's promise will not resolve, as the controller's transitionToRoute method is returning null:
Uncaught TypeError: Cannot read property 'transitionToRoute' of null
login.coffee
success: (response) ->
# ...
attemptedTransition = #get("attemptedTransition")
if attemptedTransition
attemptedTransition.retry()
#set "attemptedTransition", null
else
#transitionToRoute "dashboard"
login-test.coffee
`import {test, moduleFor} from "ember-qunit"`
moduleFor "controller:login", "LoginController", {
}
# Replace this with your real tests.
test "it exists", ->
controller = #subject()
ok controller
###
Test whether the authentication token is passed back in JSON response, with `token`
###
test "obtains authentication token", ->
expect 2
workingLogin = {
username: "user#pass.com",
password: "pass"
}
controller = #subject()
Ember.run(->
controller.setProperties({
username: "user#pass.com",
password: "pass"
})
controller.login().then(->
token = controller.get("token")
ok(controller.get("token") isnt null)
equal(controller.get("token").length, 64)
)
)
When the line #transitionToRoute("dashboard") is removed, the test passes; otherwise, the test fails.
How can I fix this error, while still maintaining my controller logic?
Work around: bypass transitionToRoute if target is null. Something like:
if (this.get('target')) {
this.transitionToRoute("dashboard");
}
I ran into the same error and dug into Ember source code a little bit. In my case this error is thrown by ControllerMixin because get(this, 'target') is null at this line. The test module probably has no idea what target should be in a controller unit test like this without further context, so you may need to manually set it or just bypass it.
Since you're not interested in the transition itself, you can just stub out the transitionToRoute method on the controller.
JS:
test('Name', function() {
var controller = this.subject();
controller.transitionToRoute = Ember.K;
...
}
Coffee:
test "it exists", ->
controller = #subject()
controller.transitionToRoute = Ember.K
ok controller
Not sure why transitionToRoute method is undefined when you execute it within an unit test - it is probably related to the fact that the execution context is different.
One possible workaround to this would be if you move your transitionToRoute call to the route instead of it being in the controller. That way your controller will send action to its route and you'll keep routing only in the route.
There is a big discussion around which is better practice - routing from controller or not but this is another story.
I want to write a unit test that should check if an unauthenticated user can view the user list (which he shouldnt be able to).
My routes
Route::group(array('prefix' => 'admin'), function() {
Route::get('login', function() {
return View::make('auth.login');
});
Route::post('login', function() {
Auth::attempt( array('email' => Input::get('email'), 'password' => Input::get('password')) );
return Redirect::intended('admin');
});
Route::get('logout', 'AuthController#logout');
Route::group(array('before' => 'auth'), function() {
Route::get('/', function() {
return Redirect::to('admin/users');
});
Route::resource('users', 'UsersController');
});
});
My test
public function testUnauthenticatedUserIndexAccess() {
$response = $this->call('GET', 'admin/users');
$this->assertRedirectedTo('admin/login');
}
My filter
Route::filter('auth', function() {
if (Auth::guest()) return Redirect::guest('admin/login');
});
Result
Failed asserting that Illuminate\Http\Response Object (...) is an instance of class "Illuminate\Http\RedirectResponse".
If i log the $response from the test, it shows the full user list like if an admin was logged in during testing.
If i browse to admin/users using a browser without logging in I'm redirected to login like i should, so the auth filter is indeed working.
Questions
Is there something in Laravel that logs in the first user during testing for you by default? Or is Auth::guest() always false by default during testing?
If so, how do i become "logged out" during unit testing? I tried $this->be(null) but got an error saying the object passed must implement UserInterface.
Laravel filters are automatically disabled for unit tests; you need to enable them for a specific test case.
Route::enableFilters();
Or this if you're not keen on using the static facades
$this->app['router']->enableFilters();
Take a look in the unit testing documentation.
Your test should now return the correct object type allowing your test to pass.