Testing Caveats

Testing is extremely important to give you confidence in your code. VeeValidate isn't special when it comes to testing, but a lot of issues seemed to miss a few important details about testing.

This guide illustrates the caveats that you might encounter with vee-validate.

TIP

The following examples will use vue-test-utils API to conduct the tests.

Asynchronous Testing

VeeValidate is primarily asynchronous, so you would need to disable vue-test-utils sync mode. And you will use flush-promises to wait for the updated to take effect.

To disable the sync mode, when mounting the component set the sync option to false in the mounting options.

const wrapper = mount(MyComponent, { sync: false });

After triggering an event like an input event, make sure to call flushPromises before checking the UI for changes:

await flushPromises();

Don't test implementation

Generally you want to test how your UI behaves rather than how it does it.

“I don’t care how you come up with the answer, just make sure that the answer is correct under this set of circumstances”

Consider this test for example, we have this component:

<div>
  <ValidationProvider rules="required" v-slot="{ errors }" ref="provider">
    <input v-model="value" type="text">
    <span class="error">{{ errors[0] }}</span>
  </ValidationProvider>
</div>

A test case could look like this:

const wrapper = mount(MyComponent, { sync: false });

wrapper.find('input').setValue('');
// flush the pending validation.
await flushPromises();
// Get the error message from the ref
const error = wrapper.vm.$refs.provider.errors[0];
expect(error).toBeTruthy();

While this test would work fine, it could fail easily, since messages is internal property and may change at anytime without notice and also if you decided to swap vee-validate for something else it will immediately break, you want to test your UI the way the user would use it.

So instead of checking the internal messages, let's describe what the user will actually experience via a short story and simulate that.

"The user should see an error when they input an empty value"

You could test that like this,

const wrapper = mount(MyComponent, { sync: false });

wrapper.find('input').setValue('');
// flush the pending validation.
await flushPromises();

// Check if the element has text inside it.
const errorEl = wrapper.find('.error');
expect(error.text()).toBeTruthy();

This test is much more robust than the previous one, if you were to change the validation your tests would still work. Because we are not testing implementation details, we care if the validation is done, we don't care how it is being done.

Testing Error Messages

Following upon the previous idea, testing specific error messages is also very flaky. Assume you have a language for which your messages may change at any time, for example they are generated by a backend service, or they can be improved as you iterate through your app. You want to keep improving those messages grammar-wise or style wise.

For example, you could have this in your tests:

expect(error.text()).toBe('The name field is required');

The problem with this, is that you may change the style to This field is required. Or even change the field name for something less ambiguous like full name. Ask your self this: do you really care if that specific message is displayed? or are you are only interested in having a message appear to your user?

Depending on your answer, you might change how the test is conducted.

If you want to check if any message is going to be displayed, any of these would do:

// Any message
expect(error.text()).toBeTruthy();

// we only care if the message has `required`.
expect(error.text()).toContain('required');

Testing ValidationObserver Debounced State

The ValidationObserver state is computed every 16ms. So if you have a template like this:

<ValidationObserver ref="observer" v-slot="{ dirty }">
  <ValidationProvider
    ref="provider"
  >
    ...
  </ValidationProvider>

  <button
    :disabled="!dirty"
    data-test="button"
  >
    My button
  </button>
</ValidationObserver>

And you want to test if the button is actually disabled, you could try to do the following:

const button = wrapper.find('[data-test="button"]');
expect(button.attributes().disabled).toBeUndefined();

Which fails due to the ValidationObserver not having the state updated yet, you would need to wait for 16+ ms for your test to work.

To work around this, you should make use of jest.useFakeTimers() or any timer mocking functions that can be used to mock passed time.

First you need to inject the mocked timers:

// before running tests
jest.useFakeTimers();

And before anytime you test something that involves the ValidationObserver state, you use:

jest.advanceTimersByTime(50);

// or
jest.runAllTimers();

You can also write your own flush method that should make waiting for async operations much easier:

async function flush() {
  await flushPromises();
  jest.runAllTimers();
}