四、测试属性和自定义事件

测试属性、事件和自定义事件有不同的方法。

属性是从父组件传递到子组件的自定义属性。自定义事件的作用正好相反:它们通过事件将数据发送到直接父级。当它们结合在一起时,它们是 Vue.js 组件中交互和通信的导线。

在单元测试中,测试输入和输出(属性和自定义事件)意味着测试组件在隔离地接收和发送数据时的行为。所以,让我们把手弄脏。

性质

当我们测试组件属性时,我们可以测试组件在传递某些属性时的行为。然而,在继续之前,请考虑一下这个重要的注意事项:

要将属性传递给组件,请使用propsData而不是props。后者用于定义属性,而不是传递数据。

首先,创建一个Message.test.js文件并添加以下代码:

describe("Message.test.js", () => {
  let cmp;
  describe("Properties", () => {
    // @TODO
  });
});

我们将测试用例分组在一个describe表达式中,它们可以嵌套。因此,我们可以使用此策略分别对属性和事件的测试进行分组。

然后,我们将创建一个帮助器工厂函数来创建消息组件,并为其提供一些属性:

const createCmp = propsData => mount(Message, { propsData });

测试性能是否存在

我们可以测试的最明显的事情是属性是否存在。记住,Message.vue组件有一个message属性,所以我们假设它正确地接收到该属性。vue test utils 带有一个hasProp(prop, value)功能,在这种情况下非常方便:

it("has a message property", () => {
  cmp = createCmp({ message: "hey" });
  expect(cmp.hasProp("message", "hey")).toBeTruthy();
});

属性的行为方式是,只有在组件中声明属性时才会接收它们。这意味着如果我们传递一个未定义的属性,那么它将不会被接收。因此,要检查属性是否不存在,可以使用不存在的属性:

it("has no cat property", () => {
  cmp = createCmp({ cat: "hey" });
  expect(cmp.hasProp("cat", "hey")).toBeFalsy();
});

但是,在这种情况下,测试将失败,因为 Vue 具有非道具属性https://vuejs.org/v2/guide/components.html#Non-道具属性。这将其设置为Message组件的根,因此被识别为道具,因此测试将返回true。将其更改为toBeTruty将使此示例通过:

it("has no cat property", () => {
  cmp = createCmp({ cat: "hey" });
  expect(cmp.hasProp("cat", "hey")).toBeTruthy();
});

我们也可以测试默认值。转到Message.vue并更改props如下:

props: {
  message: String,
  author: {
    type: String,
    default: 'Paco'
  }
},

然后,测试可以如下所示:

it("Paco is the default author", () => {
  cmp = createCmp({ message: "hey" });
  expect(cmp.hasProp("author", "Paco")).toBeTruthy();
});

断言属性验证

属性可以具有验证规则,以确保属性是必需的或是确定的类型。让我们将message属性写如下:

props: {
  message: {
    type: String,
    required: true,
    validator: message => message.length > 1
  }
}

更进一步,您可以使用自定义构造函数类型或自定义验证规则,如您在the documentation中所见 https://vuejs.org/v2/guide/components.html#Prop-验证。现在不要这样做;我只是举个例子:

class Message {}

props: {
  message: {
    type: Message, // It's compared using instance of
    ...
    }
  }
}

每当未满足验证规则时,Vue 将显示一个console.error。例如,对于createCmp({ message: 1 }),错误如下:

 [Vue warn]: Invalid prop: type check failed for prop "message". Expected String, got Number.
(found in <Root>)

在撰写本文时,vue-test-utils没有一个实用程序来测试这一点。我们可以使用jest.spyOn来测试它:

it("message is of type string", () => {
  let spy = jest.spyOn(console, "error");
  cmp = createCmp({ message: 1 });
  expect(spy).toBeCalledWith(
    expect.stringContaining("[Vue warn]: Invalid prop")
  );
  spy.mockReset(); // or mockRestore() to completely remove the mock
});

在这里,我们监视console.error函数,并检查它是否显示包含特定字符串的消息。这不是一个理想的检查方法,因为我们在监视全局对象并依赖于副作用。

幸运的是,有一种更简单的方法可以做到这一点,那就是选中vm.$options。这里是 Vue 存储展开的组件选项的地方。通过扩展,我的意思是可以用不同的方式定义属性:

props: ["message"];
// or
props: {
  message: String;
}
// or
props: {
  message: {
    type: String;
  }
}

但它们最终都将以最扩展的对象形式结束(例如最后一个)。因此,如果我们检查第一个案例的cmp.vm.$option.props.message,它们都将是{ type: X }格式(尽管对于第一个示例,它将是{ type: null}

考虑到这一点,我们可以编写一个测试套件来测试message属性是否具有预期的验证规则:

describe('Message.test.js', () => {
  ...
  describe('Properties', () => {
    ...
    describe('Validation', () => {
      const message = createCmp().vm.$options.props.message
      it('message is of type string', () => {
       expect(message.type).toBe(String)
      })
      it('message is required', () => {
        expect(message.required).toBeTruthy()
      })
      it('message has at least length 2', () => {
        expect(message.validator && message.validator('a')).toBeFalsy()
        expect(message.validator && message.validator('aa')).toBeTruthy()
      })
    })

定制活动

我们可以在自定义事件中至少测试两件事:

  • 断言在操作之后触发事件
  • 检查事件侦听器在被触发时是否调用

MessageList.vueMessage.vue组件示例中,这将转换为以下内容:

  • 断言Message组件在单击消息时触发message-clicked
  • 检查MessageList以确保在触发message-clicked时调用handleMessageClick函数

首先,转到Message.vue并使用$emit触发该自定义事件:

<template>
  <li
    style="margin-top: 10px"
    class="message"
    @click="handleClick">
      {{message}}
  </li>
</template>
<script>
export default {
  name: "Message",
  props: ["message"],
  methods: {
    handleClick() {
      this.$emit("message-clicked", this.message)
    }
  }
};
</script>

然后,在MessageList.vue中,使用@message-clicked处理事件:

<template>
  <ul>
    <Message
      @message-clicked="handleMessageClick"
      :message="message"
      v-for="message in messages"
      :key="message"/>
  </ul>
</template>
<script>
import Message from "./Message";
export default {
  name: "MessageList",
  props: ["messages"],
  methods: {
    handleMessageClick(message) {
      console.log(message)
    }
  },
  components: {
    Message
  }
};
</script>

现在是编写单元测试的时候了。在test/Message.spec.js文件中创建一个嵌套的describe并准备断言Message组件在点击我们前面提到的消息时触发message-clicked的裸骨:

describe("Message.test.js", () => {
  describe("Events", () => {
    beforeEach(() => {
      cmp = createCmp({ message: "Cat" });
    });
    it("calls handleClick when click on message", () => {
      // @TODO
    });
  });
});

测试事件点击是否调用方法处理程序

我们可以测试的第一件事是,当点击一条消息时,handleClick函数被调用。为此,我们可以使用包装器组件的trigger和使用spyOn函数的 Jest spy:

it("calls handleClick when click on message", () => {
  const spy = spyOn(cmp.vm, "handleClick");
  cmp.update(); // Forces to re-render, applying changes on template
  const el = cmp.find(".message").trigger("click");
  expect(cmp.vm.handleClick).toBeCalled();
});

参见cmp.update()。当我们更改模板中使用的内容时-handleClick,在本例中-,我们希望模板应用这些更改,我们需要使用update函数。

请记住,通过使用间谍,将调用原始的handleClick方法。你可能会故意想要这样;但是,通常,我们希望避免这种情况,只需检查单击时是否确实调用了该方法。为此,我们可以使用 Jest 模拟函数:

it("calls handleClick when click on message", () => {
  cmp.vm.handleClick = jest.fn();
  cmp.update();
  const el = cmp.find(".message").trigger("click");
  expect(cmp.vm.handleClick).toBeCalled();
});

在这里,我们完全替换了handleClick方法,该方法可以在mount函数返回的包装器组件的vm上访问。

通过使用官方工具提供的setMethods助手,我们可以更加轻松:

it("calls handleClick when click on message", () => {
  const stub = jest.spy();
  cmp.setMethods({ handleClick: stub });
  cmp.update();
  const el = cmp.find(".message").trigger("click");
  expect(stub).toBeCalled();
});

使用setMethods是建议的方法,因为这是官方工具为我们提供的一种抽象,以防 Vue 内部发生变化。

测试点击的自定义事件消息是否发出

我们已经测试了 click 方法调用它的处理程序,但是还没有测试处理程序本身是否发出message-clicked事件。我们可以直接调用handleClick方法,结合 Vuevm``$on方法使用 Jest Mock 函数:

it("triggers a message-clicked event when a handleClick method is called", () => {
  const stub = jest.fn();
  cmp.vm.$on("message-clicked", stub);
  cmp.vm.handleClick();
  expect(stub).toBeCalledWith("Cat");
});

请看,这里我们使用的是toBeCalledWith,因此我们可以准确地断言我们期望的参数,从而使测试更加健壮。这并不是说我们在这里没有使用cmp.update(),因为我们没有进行任何需要传播到模板的更改。

测试@message clicked 是否触发事件

对于自定义事件,我们不能使用trigger方法,因为它只用于 DOM 事件。但是,我们可以通过获取Message组件并使用其vm.$emit方法,自行发出事件。因此,在MessageList.test.js中增加以下测试:

it("Calls handleMessageClick when @message-click happens", () => {
  const stub = jest.fn();
  cmp.setMethods({ handleMessageClick: stub });
  cmp.update();
  const el = cmp.find(Message).vm.$emit("message-clicked", "cat");
  expect(stub).toBeCalledWith("cat");
});

我将让你来测试handleMessageClicked的功能。

收工

在本章中,我们探讨了几个测试属性和事件的案例。官方的 Vue 测试工具vue-test-utils确实让这变得容易多了。

您可以在GitHub上找到我们在这里使用的工作代码 https://github.com/alexjoverm/vue-testing-series/tree/Test-Properties-and-Custom-Events-in-Vue-js-Components-with-Jest )。