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

Add lifecycle hooks mocking #167

Closed
wants to merge 8 commits into from
Closed

Conversation

wtho
Copy link
Contributor

@wtho wtho commented Nov 11, 2017

This solves #166

  • Added test case for new functionality
  • npm test passes

It will allow the test author to easily mock lifecycle hooks. Works for all hooks.

Example:

mount(TestedComponent, {
  mocks: {
    mounted() {
      console.log('This is the mocked mounted hook!')
    }
  }
})

@wtho
Copy link
Contributor Author

wtho commented Nov 11, 2017

@eddyerburgh You should have a look at the test and give me some feedback. It uses a for-loop to iterate through all lifecycles and runs the expect-part inside the loop. This makes the test source code shorter, but gives really bad feedback in case just one of the lifecycle hooks does not pass.
An alternative would be to generate a test case it(...) for each lifecycle hook in a forEach or loop, still using the same wrapper. This would make the failing test feedback more readable. On the other hand, this pollutes the test-runner output 10x more.
Last option is to create a wrapper for each lifecycle hook, also slowing down the test suite.

Anyways, the test currently fails, because the updated-hook is not called on my local machine when using wrapper.update(), so I call it manually on the vm-object. When using the update-function, it is called on the circleci-container, which confuses me a lot.
Now I am not sure how I should construct the test case. To accept both setups or to only accept circleci's.
Why it is behaving differently at all is a mystery to me.

@wtho
Copy link
Contributor Author

wtho commented Nov 11, 2017

So the issue was, that the lifecycle-hook updated was called by Vue in v2.0.8, but not in the other (higher) tested versions.
I will make the test also accept the method to get called twice for updated.

@wtho
Copy link
Contributor Author

wtho commented Nov 11, 2017

The last commit passes in my fork repo using circleci. No idea why the package resolution is failing here.
Maybe it just needs to be rebuilt.

@eddyerburgh
Copy link
Member

We'll need to add some info in the docs that using a lifecycle hook name in mocks will overwrite the lifecycle hook

wrapper.setData({})

// call methods that will not be triggered by mount, setData and destroy manually
wrapper.vm.updated() // only called in v2.0.8
Copy link
Member

Choose a reason for hiding this comment

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

Why is it called in 2.0.8?

Copy link
Member

Choose a reason for hiding this comment

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

It would be good to figure out why it does this, or add a warning if a user is using Vue 2.0.x if they try to stub updated.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, it is poorly phrased.
I wanted to simulate behavior that would call each lifecycle-hook exactly once, but my Vue-knowledge is just not deep enough to do so. So I ended up using this combination of mount, setData and destroy to get as many as possible called.
What happened now is that the lifecycle-hooks activated, deactivated and updated were not called, that is why I had to call them manually to verify if the replacing spies are used.
Now in Vue 2.0.8 the updated method gets called, even if there is no actual change, in > 2.1 it does not get called, I guess it compares the virtual dom to figure out that nothing gets updated and therefore does not call that lifecycle hook. In 2.4.2 (my local version) it did not get called, but in the test:compatibility it also runs on v2.0.8 and therefore the hook gets called in the test. I still call it 'manually' via wrapper.vm.updated(), and need to check if it was called once or twice (line 126).

It might be a bit overkill, to keep it simple we could just call all methods directly and check on called and notCalled, if you prefer this. It was my first approach as I like to make the tests as specific as possible, therefore the calledOnce and the poorly-phrased comment. I will expand the comment a bit to make it understanding this issue easier.

@wtho
Copy link
Contributor Author

wtho commented Nov 19, 2017

I am not sure if the mocks object is the right one to use for the test author, as mocks was more meant to be used for global, not component related objects.
Another possibility I can think of is installing the hooks on the methods object

mount(TestedComponent, {
  methods: {
    mounted() {
      console.log('This is the mocked mounted hook!')
    }
  }
})

@lmiller1990
Copy link
Member

lmiller1990 commented Nov 20, 2017

Can I ask what the use case for this feature is? Test lifecycle hooks... sounds like we are testing Vue, not our own code.

I have some components that dispatch in created, but in that case, I just mock the method I want to call, let created get called as normal, and assert my mocked method was called.

Sorry to chime in late, but I'm interested in the use case behind mocking lifecycle events - I don't see the problem it solves.

@eddyerburgh
Copy link
Member

I agree with @lmiller1990 , what is the use case?

@wtho
Copy link
Contributor Author

wtho commented Nov 20, 2017

I thought less about testing the lifecycle hooks, but mocking them, replacing them with dumb functions.

E. g. when the lifecycle hook might have side effects on objects, that the tested part depends on, and therefore it would be easier to override them, to shut them.

Maybe the mounted hook will try to access objects that lead to exceptions, although we want to focus on a completely different part.

I have some components that dispatch in created, but in that case, I just mock the method I want to call, let created get called as normal, and assert my mocked method was called.

Obviously it will be possible to extract the logic in a myMounted method to override it, but this is not what one would want, if they are only a few, non-reused lines of code. Also the option of mocking all side-effected instances might be a possibility, but sometimes a much more complicated one.

But I agree, there are not too many use cases and most of the times it can be worked around with a little more work or better design. I just saw the issue popping up several times in issues and forums and found the solution to this after digging just a little.

If you think it is unnecessary, feel free to close the PR.

If not, tell me, I think I have to rewrite the tests a bit. The way I call the deactivated/activated/updated hooks seems not as the framework does it. Have to dig deeper to the vuejs-code to make the tests more 'realistic'. The mocking itself works though.

@eddyerburgh
Copy link
Member

That's a valid use case.

I think mocks is a good option to use, even though it does perform some magic, i.e. it treats lifecycle hooks differently from other mocks. I can imagine people trying to stub lifecycle hooks using the mocks method to see if it works for lifecycle hooks.

Thanks for your work so far 😄

The lifecycle hook test is adjusted to check the hook injection via
mixins is not touched by the hook override.

Furthermore the way the udpate and updated hooks are called is using the
vm.$forceUpdate method now instead of wrapper.setData, as it
consistently calls the hooks through all versions.

An callback through vm.$nextTick was added to let the update hooks be
called, which makes the test an async one using mocha's done method.
@wtho
Copy link
Contributor Author

wtho commented Nov 26, 2017

Test adjustments

It now does not have the exception for the updated hook that behaved differently in v2.0.8. I achieved this using vm.$forceUpdate instead of wrapper.setData.

Furthermore I call the lifecycle hooks activated and deactivated, that I did not manage to be called through the component creation/manipulation. I now call them the way vue calls hooks.

The test runs the checks for each lifecycle hook in a single test to enhance test performance. To ensure the tester knows which lifecycle hook failed, error descriptions are added.

Documentation

I added descriptions to the docs for the mount object, and also a short section in common-tips.

I did not add the description for docs/ja/..., docs/pt-br/... and docs/zh-cn/.... I could add the modified code snippets though.

Looking forward to your suggestions!

@@ -84,4 +84,65 @@ describe('mount.mocks', () => {
const freshWrapper = mount(Component)
expect(typeof freshWrapper.vm.$store).to.equal('undefined')
})

it('replaces lifecycle hooks, but not mixin hooks with mocks', (done) => {
Copy link
Member

Choose a reason for hiding this comment

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

Could you split this into two tests, one for the lifecycle hooks, and one that checks mixin hooks aren't replaced

The two resulting tests are:
* test if mocks replace the original lifecycle hooks
* test if hooks installed by mixins are still called, if hooks of
  component are mocked
@eddyerburgh
Copy link
Member

I don't think we're going to go ahead with this, as we want to avoid editing the internals. Thanks for the PR though 🙂

@jackchoumine
Copy link

jackchoumine commented Jul 22, 2023

Can I ask what the use case for this feature is? Test lifecycle hooks... sounds like we are testing Vue, not our own code.

I have some components that dispatch in created, but in that case, I just mock the method I want to call, let created get called as normal, and assert my mocked method was called.

Sorry to chime in late, but I'm interested in the use case behind mocking lifecycle events - I don't see the problem it solves.

Hello, I have a problem,how do test the funtion called in lifecycle hook like this?

import { shallowMount } from '@vue/test-utils'

const CounterDemo = {
  template: '<div>{{count}}</div>',
  data: () => ({
    count: 0,
    timer: null,
  }),
  mounted() {
    this.start()
  },
  destroyed() {
    this.stop()
  },
  methods: {
    start() {
      this.timer = setInterval(() => {
        this.count += 1
      }, 1000)
    },
    stop() {
      clearInterval(this.timer)
    },
  },
}
describe('CounterDemo', () => {
  it('test call start when mounted', () => {
    // Matcher error: received value must be a mock or spy function  ❌
    jest.spyOn(CounterDemo.methods, 'start')
    const wrapper = shallowMount(CounterDemo)
    expect(wrapper.vm.start).toHaveBeenCalledTimes(1)
  })
  it('test call stop when destroy', () => {
    // works well   ✅
    const wrapper = shallowMount(CounterDemo)
    jest.spyOn(wrapper.vm, 'stop')
    wrapper.destroy()
    expect(wrapper.vm.stop).toHaveBeenCalledTimes(1)
  })
})

I want to test start and stop call or not, my test case code is wrong? How can i do better in this case?
I am new vue test.

@lmiller1990
Copy link
Member

You could just wait for the count to increment - no need to mess about with spy / lifecycle methods.

it('test call start when mounted', (done) => {
  // Matcher error: received value must be a mock or spy function  ❌
  const wrapper = mount(CounterDemo)
  setTimeout(() => {
    expect(wrapper.vm.count).toBe(1)
    // or 
    expect(wrapper.find('div').textContent).toBe("1")
    done()
  }, 1200)
})

If you don't like waiting, I think Jest has some way to immediately run all timers: https://jestjs.io/docs/timer-mocks

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.

None yet

4 participants