Vuex Store 在测试之间以某种方式保持状态

Vuex Store keeps state somehow between tests

我在测试项目的前端部分时遇到一个奇怪的问题。我在前端使用 Vue 组件。该项目是教师为助手设置约会的网站,因此助手可以为 class.

准备好一切

首先让我解释一下结构。

我有一个按日期列出所有约会的组件。每个日期都是一张单独的卡片,一个日期的所有约会都是该卡片上的行。每行是一个特定的时隙。通过单击卡片顶部的按钮或单击行号,可以将约会添加到列表中。

所以我创建了三个组件:AppointmentsList.Vue 从后端获取约会并构建卡片列表,AppointmentsCard.Vue,它接收日期和该日期的所有约会作为道具,最后是 AppointmentRow.Vue,它在 table 的一行中显示约会的详细信息。状态、约会、请求的日期和其他数据保存在 Vuex 存储中。

我使用 TDD 构建项目,使用 Jest 和 Vue-test-utils 编写测试。 Mocks 用于模拟后端的响应。用卡片和行显示约会的测试工作正常。但是在测试按钮时遇到了一个奇怪的问题。

在下面的代码中,我向您展示了我的测试,为简洁起见进行了编辑。首先导入所有内容,然后定义后端对各种端点的响应。只有 appointmentResponse 很重要。请注意,有两个约会是 returned。

函数 createStore 从模块中构建一个商店。我将所有状态、吸气剂和突变保存在模块中。在每个测试 运行 之前,我创建一个新商店并使用商店的突变使用响应数据初始化商店。每次测试后,jest mocks 被清除,vue-test-utils 包装器被销毁。

*AppointmentList.spec.js*

import {mount, createLocalVue} from '@vue/test-utils'
import flushPromises from 'flush-promises'
import AppointmentsList from '../../resources/js/components/AppointmentsList.vue'
import axios from 'axios'
import Vuex from 'vuex'
import appointmentsmodule from '../../resources/js/storemodules/appointmentsModule.js'
import classhoursmodule from '../../resources/js/storemodules/classhoursModule.js'
import classroomsmodule from '../../resources/js/storemodules/classroomsModule.js'
import experimentsmodule from '../../resources/js/storemodules/experimentsModule.js'
import locationsmodule from '../../resources/js/storemodules/locationsModule.js'
import subjectsmodule from '../../resources/js/storemodules/subjectsModule.js'
import usersmodule from '../../resources/js/storemodules/usersModule.js'

import Vue from 'vue'
import { wrap } from 'lodash'

let appointmentResponse={"status":200,"lines":2,"data":[{"id":1,"subject_id":1,"owner_id":1,"group_id":1,"appointment_at":"2021-12-29T00:00:00.000000Z","classhour_id":1,"classroom_id":1,"experiment_id":null,"short_name":"magnam","description":"Sit cum quae quae quo quo consequatur.","demo":"0","toa_preferred_id":null,"location_id":1,"created_at":"2022-01-04T15:50:26.000000Z","updated_at":"2022-01-04T15:50:26.000000Z"},{"id":2,"subject_id":2,"owner_id":1,"group_id":1,"appointment_at":"2021-12-28T00:00:00.000000Z","classhour_id":1,"classroom_id":1,"experiment_id":null,"short_name":"possimus","description":"Velit eos sed esse reprehenderit.","demo":"0","toa_preferred_id":null,"location_id":1,"created_at":"2022-01-04T15:50:26.000000Z","updated_at":"2022-01-04T15:50:26.000000Z"}]}
let userResponse={"status":200,"lines":3,"data":[{"id":1,"code":"Est","name":"Mr. Americo Mertz I","email":"user1@hetstreek.nl","actual_location":"1","registrar":"1","email_verified_at":"2022-01-04T15:50:26.000000Z","created_at":"2022-01-04T15:50:26.000000Z","updated_at":"2022-01-04T15:50:26.000000Z","department_location_id":null,"welcome_valid_until":null,"roles":[{"id":2,"name":"toa","guard_name":"web","created_at":"2022-01-04T15:50:24.000000Z","updated_at":"2022-01-04T15:50:24.000000Z","pivot":{"model_id":"1","role_id":"2","model_type":"App\Models\User"}},{"id":1,"name":"beheerder","guard_name":"web","created_at":"2022-01-04T15:50:22.000000Z","updated_at":"2022-01-04T15:50:22.000000Z","pivot":{"model_id":"1","role_id":"1","model_type":"App\Models\User"}}]},{"id":2,"code":"Est","name":"Mr. Americo Mertz I","email":"user2@hetstreek.nl","actual_location":"2","registrar":"1","email_verified_at":"2022-01-04T15:50:26.000000Z","created_at":"2022-01-04T15:50:26.000000Z","updated_at":"2022-01-04T15:50:26.000000Z","department_location_id":null,"welcome_valid_until":null,"roles":[{"id":3,"name":"docent","guard_name":"web","created_at":"2022-01-04T15:50:25.000000Z","updated_at":"2022-01-04T15:50:25.000000Z","pivot":{"model_id":"2","role_id":"3","model_type":"App\Models\User"}}]},{"id":3,"code":"Est","name":"Mr. Americo Mertz I","email":"user3@hetstreek.nl","actual_location":"1","registrar":"1","email_verified_at":"2022-01-04T15:50:26.000000Z","created_at":"2022-01-04T15:50:26.000000Z","updated_at":"2022-01-04T15:50:26.000000Z","department_location_id":"{\"department_id\":1,\"location_id\":1,\"updated_at\":\"2022-01-04T15:50:26.000000Z\",\"created_at\":\"2022-01-04T15:50:26.000000Z\",\"id\":1}","welcome_valid_until":null,"roles":[{"id":4,"name":"leerling","guard_name":"web","created_at":"2022-01-04T15:50:26.000000Z","updated_at":"2022-01-04T15:50:26.000000Z","pivot":{"model_id":"3","role_id":"4","model_type":"App\Models\User"}}]}]}
let classhourResponse={"status":200,"lines":2,"data":[{"id":1,"name":"9","starttime":"15:22","endtime":"12:56","location_id":"1","created_at":"2022-01-04T15:50:26.000000Z","updated_at":"2022-01-04T15:50:26.000000Z"},{"id":3,"name":"1","starttime":"21:47","endtime":"20:16","location_id":"1","created_at":"2022-01-04T15:50:26.000000Z","updated_at":"2022-01-04T15:50:26.000000Z"}]}
let classroomResponse={"status":200,"lines":2,"data":[{"id":1,"name":"non","number":"756","in_use":"1","student_accessible":"0","teacher_accessible":"1","location_id":"1","created_at":"2022-01-04T15:50:26.000000Z","updated_at":"2022-01-04T15:50:26.000000Z"},{"id":3,"name":"ut","number":"214","in_use":"1","student_accessible":"0","teacher_accessible":"1","location_id":"1","created_at":"2022-01-04T15:50:26.000000Z","updated_at":"2022-01-04T15:50:26.000000Z"}]}
let experimentResponse={"status":200,"lines":2,"data":[{"id":1,"name":"nam","description":"Aliquam nihil voluptas aut vel neque.","student_selectable":"0","user_id":"1","created_at":"2022-01-04T15:50:26.000000Z","updated_at":"2022-01-04T15:50:26.000000Z"},{"id":3,"name":"similique","description":"Exercitationem officiis excepturi aut veniam voluptatum.","student_selectable":"1","user_id":"1","created_at":"2022-01-04T15:50:26.000000Z","updated_at":"2022-01-04T15:50:26.000000Z"}]}
let locationResponse={"status":200,"lines":2,"data":[{"id":1,"name":"Omnis.","school_id":"1","created_at":"2022-01-04T15:50:26.000000Z","updated_at":"2022-01-04T15:50:26.000000Z"},{"id":2,"name":"Illo.","school_id":"1","created_at":"2022-01-04T15:50:26.000000Z","updated_at":"2022-01-04T15:50:26.000000Z"}]}
let schoolResponse={"status":200,"lines":1,"data":[{"id":1,"schoolname":"Jaydon Mante","domain":"hetstreek.nl","max_locations":"1","payed_at":null,"due_date":null,"active":"1","storage_folder":"jaydonmante_hetstreeknl","created_at":"2022-01-05T14:41:49.000000Z","updated_at":"2022-01-05T14:41:49.000000Z"}]}
let subjectResponse={"status":200,"lines":2,"data":[{"id":1,"code":"SK","description":"Quis.","block_for_days":"9","color":"A3A3A3","created_at":"2022-01-05T18:54:41.000000Z","updated_at":"2022-01-05T18:54:41.000000Z"},{"id":2,"code":"re","description":"Libero.","block_for_days":"3","color":"A3A3A3","created_at":"2022-01-05T18:54:41.000000Z","updated_at":"2022-01-05T18:54:41.000000Z"}]}

function createStore(){
    return {
        modules:{
            appointments:appointmentsmodule,
            classhours:classhoursmodule,
            classrooms:classroomsmodule,
            experiments:experimentsmodule,
            locations:locationsmodule,
            subjects: subjectsmodule,
            users: usersmodule
        }
    }
}
let wrapper
let store
beforeEach(()=>{
    const newStore=createStore()
    //define a new store for each test
    store=new Vuex.Store(newStore)
    //sets the range of dates for which the appointments need to be shown.
    let startdate = new Date(2021, 11, 27)
    let enddate = new Date(startdate)
    enddate.setDate(startdate.getDate()+3)
    //initialize the store
    store.commit('setAppointmentsPeriod',{startdate,enddate})
    store.commit('setSubjectFilter', [])
    store.commit('storeAppointments',[])
    store.commit('storeClasshours',classhourResponse.data)
    store.commit('storeClassrooms',classroomResponse.data)
    store.commit('storeExperiments',experimentResponse.data)
    store.commit('storeLocations',locationResponse.data)
    store.commit('storeSubjects',subjectResponse.data)
    store.commit('storeUsers',userResponse.data)
    //log to show the appointments are empty
    console.log(store.state.appointments.appointments)

    jest.clearAllMocks()
    
})

afterEach(()=>{
    jest.clearAllMocks
    wrapper.destroy()
})

jest.mock("axios")

const localVue =createLocalVue()
localVue.use(Vuex)

当组件 AppointmentsList 在测试中安装时,它会检测存储中的时间段变化,这会触发对后端的请求并加载该时间段的约会。

第一个测试测试卡片顶部的按钮。它触发按钮并验证添加约会的模式已打开,填写表单,触发提交按钮并验证数据是否已发送到后端,并且商店已更新,所以现在商店中应该有三个约会。这很好用。

test('add button adds appointment to list', async ()=>{
        //set up the mock data.
        let classhour=1
        let appointmentToAdd={
            id:3,
            owner_id:1,
            experiment_id:null,
            short_name:"Schaduwpracticum",
            description:"Lichtkastje met voeding, kartonnetje, scherm, liniaal",
            group_id:1,
            subject_id:1,
            toa_preferred_id:1,
            appointment_at:"2021-12-27",
            classhour_id:classhour,
            classroom_id:1,
            demo:false,
            location_id:1,
            created_at:new Date(),
            updated_at:new Date()
        }
        //mock axios responses. Get returns the appointments, post returns the added appointment. 
        axios.get.mockResolvedValue({status:200 , data:appointmentResponse.data})
        axios.post.mockResolvedValue({status:200, data:{"status":200, "lines":1,"data":appointmentToAdd}})
        //appointment is added for this date.
        let checkDate=new Date(2021,11,27)
        
        //show all two appointments
        wrapper = mount(AppointmentsList, {store, localVue})
        await flushPromises()
        //verify two appointments in the store
        expect(store.state.appointments.appointments).toHaveLength(2)
        //find add button for first date
        //click buttons open add/edit modal with date field prefilled
        await wrapper.find('[data-cy="20211227"]').trigger('click')

        const wrappedAddAppointment=wrapper.findComponent({name:'add-appointment'})
        //check modal is opened with correct date
        expect(wrappedAddAppointment.vm.$props.modalState).toBe(true)
        expect(wrappedAddAppointment.vm.$data.appointment.appointment_at).toStrictEqual(checkDate)
        //fill in fields and click submit
        await wrappedAddAppointment.find('input[id="shortname"]').setValue(appointmentToAdd.short_name)
        await wrappedAddAppointment.find('input[id="description"]').setValue(appointmentToAdd.desc)
        await wrappedAddAppointment.find('input[id="teacher"]').setValue(1)
        await wrappedAddAppointment.find('input[id="group"]').setValue(appointmentToAdd.group_id)
        await wrappedAddAppointment.find('input[id="subject"]').setValue(appointmentToAdd.subject_id)
        await wrappedAddAppointment.find('input[id="preferredtoa"]').setValue(appointmentToAdd.toa_preferred_id)
        await wrappedAddAppointment.find('input[id="appointment_at"]').setValue(appointmentToAdd.appointment_at)
        await wrappedAddAppointment.find('input[id="classhour"]').setValue(appointmentToAdd.classhour_id)
        await wrappedAddAppointment.find('input[id="classroom"]').setValue(appointmentToAdd.classroom_id)
        await wrappedAddAppointment.find('input[id="demo"]').setChecked(false)
        await wrappedAddAppointment.find('button[name="save-button"]').trigger('click')
        //check axios post(/appointments) is called
        expect(axios.post).toHaveBeenCalledTimes(1)
        expect(axios.post.mock.calls[0][0]).toContain('/appointments')
        expect(axios.get).toHaveBeenCalledTimes(2)
        //verify the modal is closed
        expect(wrappedAddAppointment.vm.$props.modalState).toBe(false)
        //verify the appointment is added to the store
        expect(store.state.appointments.appointments).toHaveLength(3)
       
        //verify added appointment is in list
        expect(wrapper.text()).toContain(appointmentToAdd.short_name)
    })

下一个测试测试点击行号。它还应打开 AddAppointment 模式,并预先填充日期和 class 小时(=行号)。由于添加约会已经过测试,测试停止。

test('clicking classhour adds appointment to list', async ()=>{
        //setup ajax responses
        axios.get.mockResolvedValue({status:200 , data:appointmentResponse.data})
        axios.post.mockResolvedValue({status:200, data:{"status":200, "lines":1,"data":appointmentToAdd}})
        
        let checkDate=new Date(2021,11,27)
        console.log(store.state.appointments.appointments)
        //show all appointments
        const wrapper = mount(AppointmentsList, {store, localVue})
        await flushPromises()

        expect(axios.get).toHaveBeenCalledTimes(1)
-->     expect(store.state.appointments.appointments).toHaveLength(2)
        //test fails on line above. Added next two lines to check the appointments in the store and the return value of the axios call.
        console.log(store.state.appointments.appointments) //shows three appointments with the last one being the appointment added in the previous test
        console.log(axios.get.mock.results[0].value) //shows only two appointments returned as expected
        
        //find classhour button of date 27-dec-2021 and classhour 3
        await wrapper.find('[data-cy="20211227classhour3"]').trigger('click')
        
        const wrappedAddAppointment=wrapper.findComponent({name:'add-appointment'})
        
        //check modal is opened with correct date and classhour
        expect(wrappedAddAppointment.vm.$props.modalState).toBe(true)
        expect(wrappedAddAppointment.vm.$data.appointment.appointment_at).toStrictEqual(checkDate)
        expect(wrappedAddAppointment.vm.$data.appointment.classhour.id).toBe(3)
    })

此测试在标有箭头的行处失败。 Jest 报告的不是两次预约,而是三次在店内的预约。因为我在测试之间重建了商店,所以这对我来说毫无意义。添加的约会不应再在商店中。模拟电话在响应中清楚地显示了两个约会。但它变得更加陌生。我没有将两个初始约会添加到商店,而是决定将响应更改为 return 没有约会。我将模拟响应更改为:

axios.get.mockResolvedValue({status:200 , data:{status:200, lines:0, data:[]} })

的验证
expect(store.state.appointments.appointments).toHaveLength(0) 

此测试通过,因此不会保留第一次测试中添加的约会。 谁能帮我解释一下?

编辑。根据评论中的要求添加了 appointmentModule。

const appointmentsmodule = {
    state(){
        return{
            appointments:[],
            appointmentPeriod:{
                startdate:null,
                enddate:null
            },
            subjectFilter:[],
        }
    },
    mutations:{
        storeAppointments(state, appointments){
            //console.log('storeAppointments', appointments)
            state.appointments = appointments
        },
        setAppointmentsPeriod(state, period){
            //console.log('setAppointmentsPeriod', period)
            
            state.appointmentPeriod = period
        },
        setSubjectFilter(state, filter){
            //console.log('set Filter', filter)
            
            state.subjectFilter= filter
        },
        addAppointment(state, appointmentToAdd){
            state.appointments.push(appointmentToAdd)
        }
    },
    getters:{
        getFilteredAppointments: state=>{
            //console.log('getting filtered appointments')
            if(state.subjectFilter.length>0){
                ////console.log('filtered')
                return state.appointments.filter(appointment=>{
                    /* //console.log(state.subjectFilter)
                    //console.log(appointment.subject_id)
                    //console.log(state.subjectFilter.includes(appointment.subject_id)) */
                    return state.subjectFilter.includes(appointment.subject_id)})
            }
            else{
                ////console.log('no filter')
                return state.appointments
            }
        },
        getAppointmentsPeriod(state){
            //console.log('getting  appointments period')
            
            return state.appointmentPeriod
        }
    }
}

export default appointmentsmodule

我解决了这个问题。当存储来自后端的响应时,我用新数组替换了 vuex.state 中的数组,从而破坏了反应性。 当我使用

state.appointments.splice(0,Infinity, ...newAppointments)

原始数组的所有元素都被新元素替换。保留反应性并通过测试