So I know that shallow allows you to isolate testing behavior of the component (and not its children). And that mounting renders the entire tree (parent and children, and its children, lifecycle methods on each, and so on).
But I don't understand what is exactly going on here:
it('stores the session and token on login', async () => {
store = initStore()
const loginContainer = shallow(<LoginContainer store={store} />)
await loginContainer.find('LoginContainer').props().authenticate(fakeEmail, fakePassword)
const { isAuthenticated, isAuthenticating, token, session } = loginContainer.find('LoginContainer').props()
expect(isAuthenticating).to.be.false
expect(isAuthenticated).to.be.true
expect(session).to.equal(fakeSession)
expect(token).to.equal(fakeToken)
})
If I mount it, then both isAuthenticating and isAuthenticated result in _false_
If I shallow it, then isAuthenticating is set to _false_ and isAuthenticated is set to _true_
Notice I'm calling authenticate() by getting at it via the props of the mounted or shallowed container.
LoginContainer
export const mapDispatchToProps = {
authenticate: AsyncActions.authenticate
}
so by calling authenticate() via my container's props(), it's simply calling my thunk action here:
export function authenticate(email, password) {
return async (dispatch) => {
dispatch(AuthActions.requestAuthentication())
try {
const response = await AuthApi.Authenticate(email, password),
session = response.body.data[0].session,
token = response.body.data[0].token
if(token){
dispatch(AuthActions.authenticated(session, token))
}
} catch (err) {
console.log(`failed request: $(err.status) /n $(err) `)
}
}
}
And the reducer:
export const initialState = {
isAuthenticating: false,
isAuthenticated: false,
token: null
}
export default function authReducer(state = initialState, action) {
switch (action.type) {
case ActionTypes.REQUEST_AUTHENTICATION: {
return {...state,
isAuthenticating: true,
isAuthenticated: false }
}
case ActionTypes.AUTHENTICATED: {
console.log('ActionTypes.AUTHENTICATED hit')
return {...state,
isAuthenticating: false,
isAuthenticated: true,
session: action.session,
token: action.token
}
}
case ActionTypes.UNAUTHENTICATED: {
return {...state,
isAuthenticating: false,
isAuthenticated: false,
token: null }
}
default:
return state
}
}
Can someone explain what's going on here?
I believe this is an intentional part of enzyme's API design.
shallow(<Foo />) is not a Foo, it's "what Foo renders" - .props(), .is(), and .children() can verify this.
mount(<Foo />) is a Foo - .props(), .is(), and .children() should also verify this.
ok thanks. Hmm ok weird. I've been having trouble with shallow vs mount because for some reason this test. If I skip or don't skip this test it's having some weird affects on _another_ test of mine. This test tests the thunk action directly while the test its affecting tests calling the thunk action authenticate() indirectly (through the mounted container's props to get at the authenticate() method)
beforeEach(() => {
store = initStore()
dispatch = spy(store, 'dispatch')
requestBody = { email: "[email protected]", password: "password" }
responseBody = { token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" }
nock('https://test.execute-api.us-west-2.amazonaws.com/develop')
.post('/users/sessions')
.reply(200, responseBody)
})
...jsx
// if I SKIP this test, then it causes my other test to pass!! wtf!
it('dispatches unauthenticate action', async () => {
console.log('dispatches unauthenticate action')
const unauthenticate = await AsyncActions.unauthenticate()
await unauthenticate(dispatch)
console.log(store.getState().auth.isAuthenticating)
console.log(store.getState().auth.isAuthenticated)
expect(dispatch.calledWith(Actions.unauthenticated())).to.be.true
})
is somehow oddly preventing this test below which doesn't even live in the spec file of the the test that's failing (below) from passing because for some reason if I run the test above my authenticated() dispatch isn't firing after my test below runs:
it('stores the session and token on login', async () => {
store = initStore()
const loginContainer = mount(<LoginContainer store={store} />)
await loginContainer.find('LoginContainer').props().authenticate(fakeEmail, fakePassword)
console.log(store.getState().auth.isAuthenticating)
console.log(store.getState().auth.isAuthenticated)
const { isAuthenticated, isAuthenticating, token, session } = loginContainer.find('LoginContainer').props()
console.log(store.getState().auth.isAuthenticating)
console.log(store.getState().auth.isAuthenticated)
expect(isAuthenticating).to.be.false
expect(isAuthenticated).to.be.true
expect(session).to.equal(fakeSession)
expect(token).to.equal(fakeToken)
})
action
export function authenticate(email, password) {
return async (dispatch) => {
dispatch(AuthActions.requestAuthentication())
try {
const response = await AuthApi.Authenticate(email, password),
session = response.body.data[0].session,
token = response.body.data[0].token
if(token){
dispatch(AuthActions.authenticated(session, token))
}
} catch (err) {
console.log(`failed request: $(err.status) /n $(err) `)
}
}
}
export function unauthenticate(){
return async (dispatch) => {
dispatch(AuthActions.unauthenticated())
}
}
reducer
export default function authReducer(state = initialState, action) {
switch (action.type) {
case ActionTypes.REQUEST_AUTHENTICATION: {
console.log('ActionTypes.REQUEST_AUTHENTICATION hit')
return {...state,
isAuthenticating: true,
isAuthenticated: false }
}
case ActionTypes.AUTHENTICATED: {
console.log('ActionTypes.AUTHENTICATED hit')
return {...state,
isAuthenticating: false,
isAuthenticated: true,
session: action.session,
token: action.token
}
}
case ActionTypes.UNAUTHENTICATED: {
console.log('ActionTypes.UNAUTHENTICATED hit')
return {...state,
isAuthenticating: false,
isAuthenticated: false,
token: null }
}
default:
return state
}
}
it's so bizarre....if I skip the first test, the second test does hit dispatch(AuthActions.authenticated(session, token)) and therefore my reducer also his being run for ActionTypes.AUTHENTICATED which is what I expect, so the test passes:
isAuthenticating: false
isAuthenticated: true
but if I don't skip it, it causes my second test does not hit the ActionTypes.AUTHENTICATED in my reducer and I get no updated state on my mounted container's properties for isAutenticating and isAuthenticated so my expect fails:
isAuthenticating: false
isAuthenticated: false
why that test would affect the second is so bizarre to me.
(btw, if you add jsx after the triple backticks, you'll get nicer syntax highlighting)
Since you're using a flux store, it's highly likely that you're not emptying it out after every test. Alternatively, since your tests are synchronous but your mocked ajax thing is probably returning async, it's probably happening after the first test is finished.
In other words, you may need to save the mocked promise, and return a chain off of it in your test, so that your test is async.
I'm actually using react-redux connected containers...
here's the store helper
import { applyMiddleware, createStore } from 'redux'
import thunk from 'redux-thunk'
export default (initialState = {}) => {
let middleware = applyMiddleware(thunk)
const store = createStore(reducers, initialState, middleware)
return store
}
yea I wondered if maybe it's a timing issue...
for my second test I'm creating a new instance of the store
beforeEach(() => {
store = initStore()
loginContainer = shallow(<LoginContainer store={store}/>)
dispatch = spy(store, 'dispatch')
responseBody = { data: [{session: fakeSession, token: fakeToken }] }
nock('https://test.execute-api.us-west-2.amazonaws.com/develop')
.post('/users/sessions')
.reply(200, responseBody)
})
so I didn't think it was a problem where the store was being reused and conflicting.
since your tests are synchronous
but both tests have async on the function...
after the first test is finished
yea it's hard to tell which test is running first. I assume since the tests are async you can't rely on them running in order...of course.
it's probably happening after the first test is finished
sorry just to clarify what are you thinking might be happening after, what's the _what_ you were referring to?
mocked ajax thing
I assume you're referring to nock?
yes, both tests are async functions, which means they return a promise, but nothing inside that function is dependent on any other promise, so it's still not really async :-)
Yes, I'm saying that nock might be resolving after your test is completed.
ah what I think you're saying is this happens:
First test => reducer hit = > expect passes = > nocked request (which has no bearing here and I really shouldn't run it for this test anyway)
Second test runs => dispatch(AuthActions.requestAuthentication()) called, expects() run synchronously and fail => nock returns so this is never run which would have updated the state to be what the expects needed it to be:
if(token){
dispatch(AuthActions.authenticated(session, token))
}
and I don't think you can guarantee that the first test always runs first can you? If You have a bunch of .spec.js files, hmm.. actually maybe they do...I assume though mocha takes a top down approach and runs through each file and traverses the folder top down...so maybe they do run in order, just that yea my problem isn't something based on order (thank God) it's based on internal handling of my sync vs async code...that is internal to each test.
hmm wondering how I'd do the chaining off the nock.
ok I took nock out of the beforeEach so that my first test would not run nock. It doesn't need nock anyway, my thunk action for that one is not asnc anyway. My test passes now but I caused another test to fail...rabbit hole. Nice!
looks like having nock in beforeEach isn't such a good idea and to probably run nock inside each test and await it first.
eh oh well I'm still confused by why that first nock would prevent authenticated() from running for the second tests which it was but I'd think that because I have an await on the API call, If nock took over that call, I would assumed it would still resolve at that await there:
const response = await AuthApi.Authenticate(email, password),
session = response.body.data[0].session,
token = response.body.data[0].token
if(token){
await AuthApi.Authenticate(email, password),
}
I would assume the nock promise would have resolved here still and therefore still call authenticated():
await AuthApi.Authenticate(email, password) // nock hijacks this request, await resolves it?
I don't know I guess it's still grey to me why that first nock would have prevented the await there to resolve thus prevent the await AuthApi.Authenticate(email, password), from firing for my second test but for some reason it did affect it...
thanks for the help, appreciate it.