vuejs / vue-class-component

ES / TypeScript decorator for class-style Vue components.
MIT License
5.81k stars 429 forks source link

testing with lifecycle hooks (onMounted, ...) #502

Closed MichaelSp closed 3 years ago

MichaelSp commented 3 years ago

I tried to test my composition function which contains a lifecycle hook:

count.ts

import { onMounted, ref } from 'vue'

export default function withCount() {
  const count = ref(0)

  function increment() {
    count.value = count.value + 1
  }

  onMounted(() => {
    count.value = 23
  })

  return {
    count,
    increment
  }
}

count.spec.ts

import withCount from './count'

describe('counting', function() {

  it("should count", () => {
    const counter = withCount()

    expect(counter.count.value).toEqual(0)
    counter.increment()
    expect(counter.count.value).toEqual(1)
  })
})

Resulting in the following warning:

console.warn
    [Vue warn]: onMounted is called when there is no active component instance to be associated with. Lifecycle injection APIs can only be used during execution of setup(). If you are using async setup(), make sure to register lifecycle hooks before the first await statement.

       8 |   }
       9 | 
    > 10 |   onMounted(() => {
         |   ^
      11 |     count.value = 23
      12 |   })
      13 | 

Is it possible to get rid of the warning? Ideally it should be possible to trigger the lifecycle calls in the test.

MichaelSp commented 3 years ago

I also tried to wrap it in a setup call

count.spec.ts

import withCount from './count'
import { setup } from 'vue-class-component'

describe('counting', function() {

  it("should count", () => {
    const counter = setup( () => withCount())

    expect(counter.count).toEqual(0)
    counter.increment()
    expect(counter.count).toEqual(1)
  })
})

But that made it even worse:


Error: expect(received).toEqual(expected) // deep equality

Expected: 0
Received: undefined
ygj6 commented 3 years ago

Hi @MichaelSp, withCount is just a function, not a vue component. You can reference this test case https://github.com/vuejs/vue-class-component/blob/next/test/specs/test.spec.ts#L408

it('setup', () => {
    function useCounter() {
      const count = ref(0)

      function increment() {
        count.value++
      }

      onMounted(() => {
        increment()
      })

      return {
        count,
        increment,
      }
    }

    class App extends Vue {
      counter = setup(() => useCounter())
    }

    const { root } = mount(App)
    expect(root.counter.count).toBe(1)
    root.counter.increment()
    expect(root.counter.count).toBe(2)
  })
MichaelSp commented 3 years ago

Thanks for the link. There are some useful snippets.

I guess my point is: You cannot test the function if you don't embed it in an Vue-component and mount() it. And doing so means:

  1. a lot of boilerplate
  2. bad test performance if you do that for many simple unit tests
  3. and also it feels unnecessary to do that
ygj6 commented 3 years ago

I think since onMounted is a function imported from vue, so it is reasonable to rely on a vue-component.

MichaelSp commented 3 years ago

@ygj6 You are right, but wouldn't you agree that it's much easier to test just a plain old function instead of a function with bells and whistles around? Especially the life-cycle hooks are not a relevant part of the testing environment. They should not even come from a vue-component because the component doesn't have to exist and even if it existed (just for the purpose of wrapping the function) it would have artificially triggered life-cycle events. And there is also the argument of separation of concern embedded in the design of the Composition API that argues that you can split template and logic.

Let me propose the following API:

# count.ts
function useCounter() {
  const count = ref(0)

  function increment() {
    count.value++
  }

  onMounted(() => {
    increment()
  })

  return {
    count,
    increment,
  }
}

# count.spec.ts
it('setup', () => {
  const { fn, hooks } = setupForTest( () => useCounter(/* props */) )  // new function `setupForTest`
  expect(fn.count).toBe(0)
  hooks.onMounted()
  expect(fn.count).toBe(1)
  fn.increment()
  expect(fn.count).toBe(2)
})
ktsn commented 3 years ago

Lifecycle hooks have to depend on a vue component. Imagine you have onUpdated and onDestroy hooks. How do you test them without vue components?

If you want to just save types, you can use utils like vue-composable-tester