soumak77 / firebase-mock

Firebase mock library for writing unit tests
https://soumak77.github.io/firebase-mock
350 stars 96 forks source link

Full setup example Firestore + React + Jest #115

Open artooras opened 5 years ago

artooras commented 5 years ago

Hi. While trying to set up firebase-mock in my project, I fail to 'connect the dots'. I have the following setup.

firebaseSetup.js

import firebase from 'firebase/app'
import 'firebase/auth'
import 'firebase/firestore'

if (!firebase.apps.length) {

  var config = {
    apiKey: "...",
    authDomain: "...",
    databaseURL: "...",
    projectId: "...",
    storageBucket: "...",
    messagingSenderId: "..."
  }

  firebase.initializeApp(config)
}

const firebaseAuth = firebase.auth()
const firestore = firebase.firestore()
firestore.settings({timestampsInSnapshots: true})

export default firebase
export {
  firebaseAuth,
  firestore
}

AppBody.js

import React from 'react'
import PropTypes from 'prop-types'
import firebase, {firestore} from './firebaseSetup'

const propTypes = {
  userID: PropTypes.string
}

class AppBody extends React.Component {
  state = {
    data: undefined
  }
  componentWillMount = () => {
    const userRef = firestore.collection('users').doc(this.props.userID)
    this.dbData = userRef.collection('data')
    this.dbDataUnsubscribe = this.dbData.onSnapshot(data => this.setState({data}))
  }
  componentWillUnmount = () => {
    this.dbDataUnsubscribe()
  }
  render() {...}
}

Now, the tricky part - AppBody.test.js

import React from 'react'
import proxyquire from 'proxyquire'
import {render} from 'react-testing-library'

const AppBody = proxyquire('./AppBody.js', {
  firebase: mocksdk
})

jest.mock('./firebaseSetup.js', () => {
  const firebasemock = require('firebase-mock')
  const mockauth = new firebasemock.MockAuthentication()
  const mockfirestore = new firebasemock.MockFirestore()
  const mocksdk = new firebasemock.MockFirebaseSdk(null, () => mockauth, () => mockfirestore)
  const firebase = mocksdk.initializeApp()
  const firestore = firebase.firestore()
  const firebaseAuth = firebase.auth()
  return firebase, {firebaseAuth, firestore}
})

describe('AppBody', () => {
  it('renders', () => {
    const {getByText} = render(
      <AppBody 
        userID={'aaa'} 
      />
    )
    expect(getByText('Today')).toBeInTheDocument()
  })
})

Now, obviously, my test file is incorrect. For one, mocksdk is undefined in line 6. So, my question is, how do I setup firebase-mock correctly so that my AppBody component at least renders and retrieves initial data?

Thank you very much in advance!

balazsorban44 commented 5 years ago

I struggle to set it up as well. :thinking: A better example would be very appreciated!

Eji4h commented 5 years ago

Because you use mocksdk before declare, a mocksdk was declare at line 13. I suggest you to see tutorial at Tutorial: Integrating with jest and you don't necessary to use proxyquire when you use jest.

artooras commented 5 years ago

Thanks @Eji4h. I have removed proxyquire, and my updated AppBody.test.js looks like this:

import React from 'react'
import {render} from 'react-testing-library'

jest.mock('./firebaseSetup.js', () => {
  const firebasemock = require('firebase-mock')
  const mockauth = new firebasemock.MockAuthentication()
  const mockfirestore = new firebasemock.MockFirestore()
  const mocksdk = new firebasemock.MockFirebaseSdk(
    null, // RTDB
    () => mockauth, 
    () => mockfirestore
  )
  const firebase = mocksdk.initializeApp()
  const firestore = firebase.firestore()
  const firebaseAuth = firebase.auth()
  return firebase, {firebaseAuth, firestore}
})

describe('AppBody', () => {
  it('renders', () => {
    const {getByText} = render(
      <AppBody 
        userID={'aaa'} 
      />
    )
    expect(getByText('Today')).toBeInTheDocument()
  })
})

The return statement of my mock matches that of firebaseSetup.js, as per instructions. However, when I run my test, I get an error saying:

TypeError: this.dbData.onSnapshot is not a function

referring to this line in AppBody.js:

this.dbDataUnsubscribe = this.dbData.onSnapshot(data => this.setState({data}))

So, what am I doing wrong?

froddd commented 5 years ago

The only library that needs mocking is the 'firebase/app' one, so your mock should apply to that instead and simply return the mock SDK -- the 'instructions' (if one can call them that) are misleading:

import React from 'react'
import {render} from 'react-testing-library'

// Import firebase from your firebase init script:
import firebase from './firebaseSetup'

// Now mock 'firebase/app`:
jest.mock('firebase/app', () => {
  const firebasemock = require('firebase-mock')
  const mockauth = new firebasemock.MockAuthentication()
  const mockfirestore = new firebasemock.MockFirestore()
  return new firebasemock.MockFirebaseSdk(
    null, // RTDB
    () => mockauth, 
    () => mockfirestore
  )
})

describe('AppBody', () => {
  beforeAll(() => {
    // Add some data to your mock firebase if you need to...
    firebase.firestore().autoFlush()
    firebase.firestore().collection('collectionId').doc('docId').set({foo: 'bar'})
  })

  it('renders', () => {
    const {getByText} = render(
      <AppBody 
        userID={'aaa'} 
      />
    )
    expect(getByText('Today')).toBeInTheDocument()
  })
})
artooras commented 5 years ago

Thanks, I think I'll give it another go and try your suggestion. I have since implemented a mock of my data-handling file, but maintenance is a bit of a pain in the bum... It would be much easier if I could simply mock the firebase bit.

Is there a way to set initial data for the tests? I couldn't really find that in the docs either.

froddd commented 5 years ago

AFAIK the only way to set initial data is to manually set each document, so:

const collectionRef = firebase.firestore().collection('collectionId');
ref.doc('docId').set({foo: 'bar'});
ref.doc('anotherDocId').set({foo: 'baz'});
// etc...

If you're only testing for one user and each user has its own document, I guess you'll only have one call to set().

garyo commented 5 years ago

@froddd : I tried your suggestion above, mocking the 'firebase/app' rather than my '@/firebase-setup', but it doesn't work for me; the mocking code runs but it doesn't seem to affect the actual firebase calls my source code makes -- they're still using the un-mocked firebase. For instance, in my test code firebase.firestore().autoFlush() is not a function.

Any further hints? It would be great to see a fully worked example of firebase-mock with jest.

garyo commented 5 years ago

And if I try it a different way, I get firebase.apps.length: Cannot read property 'length' of undefined. This seems to be because firebase is now the mock object:

{ database: { [Function: MockFirebaseDatabase] ServerValue: { TIMESTAMP: [Object] } },
  auth:
       { [Function: MockFirebaseAuth]
         EmailAuthProvider: { [Function: EmailAuthProvider] PROVIDER_ID: 'password', credential: [Function] },
         GoogleAuthProvider: { [Function: GoogleAuthProvider] PROVIDER_ID: 'google.com', credential: [Function] },
         TwitterAuthProvider: { [Function: TwitterAuthProvider] PROVIDER_ID: 'twitter.com', credential: [Function] },
         FacebookAuthProvider: { [Function: FacebookAuthProvider] PROVIDER_ID: 'facebook.com', credential: [Function] },
         GithubAuthProvider: { [Function: GithubAuthProvider] PROVIDER_ID: 'github.com', credential: [Function] } },
  firestore:
       { [Function: MockFirebaseFirestore]
         FieldValue: { [Function: MockFirestoreFieldValue] delete: [Function], serverTimestamp: [Function] } },
  storage: [Function: MockFirebaseStorage],
  messaging: [Function: MockFirebaseMessaging],
  initializeApp: [Function: initializeApp] }

which as you can see doesn't have an apps member. I can work around that by checking for apps in my init code, but then I'm back to this error:

TypeError: _myFirebase.db.collection(...).onSnapshot is not a function

so maybe firebase-mock just doesn't implement what I need?

garyo commented 5 years ago

Aha, I see #81 is about missing onSnapshot(). That's core to how I use Firestore so I guess this won't work for me.

froddd commented 5 years ago

I think firebase has moved on a bit since the last commits to this mocking library -- I've just come across some more missing functionality, which I may create a PR for.

Venryx commented 4 years ago

Just wanted to mention that the missing onSnapshot() functionality is added in pull-request #130, but just hasn't been accepted yet.

In the mean-time, you can add that functionality by running npm install BrianChapman/firebase-mock#issue-81-onsnapshot.

It's worked well so far in my project.


P.S. By the way, I agree with the others that the docs really need some improvement. The tutorials are... not really tutorials because they leave out important contextual information that let you produce actual working tests.

Best would be if there were a self-contained demo tutorial that is actually runnable.

In the meantime, the link to the setup tutorial page should be made more prevalent on the readme/homepage, and it should say: "Use the mocksdk variable the same way you would use the var firebase = require("firebase/app"); variable"