jest单元测试——项目实战

jest单元测试——项目实战

  • 一、纯函数测试
  • 二、组件测试
  • 三、接口测试
  • 四、React Hook测试
  • 💥 其他的疑难杂症
  • 另:好用的方法 🌟

温故而知新:单元测试工具——JEST
包括:什么是单元测试、jest的基本配置、快照测试、mock函数、常用断言、前端单测策略等等。。

一、纯函数测试

关于纯函数的测试,之前的文章讲的蛮多了,这次重点就不在这里了,感兴趣的同学请移步 温故而知新~🎉

// demo.ts
/**
 * 比较两个数组内容是否相同
 * @param {Array} arr1 - 第一个数组
 * @param {Array} arr2 - 第二个数组
 * @returns {Boolean} - 如果两个数组内容相同,返回 true,否则返回 false
 */
export const compareArrays = (arr1: ReactText[], arr2: ReactText[]) => {
  if (arr1.length !== arr2.length) {
    return false
  } else {
    const result = arr1.every((item) => arr2.includes(item))
    return result
  }
}

//demo.test.ts
describe('compareArrays', () => {
  test('should return true if two arrays are identical', () => {
    const arr1 = [1, 2, 3]
    const arr2 = [1, 2, 3]
    expect(compareArrays(arr1, arr2)).toBe(true)
  })

  test('should return false if two arrays have different lengths', () => {
    const arr1 = [1, 2, 3]
    const arr2 = [1, 2, 3, 4]
    expect(compareArrays(arr1, arr2)).toBe(false)
  })

  // 好多好多用例,我就不每个都展示出来了
})

二、组件测试

虽然 Jest 可以对 React 组件进行测试,但不建议在组件上编写太多的测试,任何你想测试的内容,例如业务逻辑,还是建议从组件中独立出来放在单独的函数中进行函数测试,但测试一些 React 交互是很有必要的,例如要确保用户在单击某个按钮时是否正确地调用特定函数。

1. 准备工作——配置 🔧

下载 @testing-library/jest-dom 包:

npm install @testing-library/jest-dom --save-dev

同时,要在 tsconfig.json 里引入这个库的类型声明:

{
  "compilerOptions": {
    "types": ["node", "jest", "@testing-library/jest-dom"]
  }
}

为了防止引入 css 文件报错:

npm install --dev identity-obj-proxy

在项目根目录下创建jest.config.js文件:

module.exports = {
  collectCoverage: true, // 是否显示覆盖率报告
  testEnvironment: 'jsdom', // 添加 jsdom 测试环境
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
    '\\.(css|scss)$': 'identity-obj-proxy',
  },
}

2. 开始测试——写用例 📝

先用小小的 button 试试水~

describe('Button component', () => {
  // 测试按钮文案
  test('should have correct text content', () => {
    const { getByText } = render(<button>Click me</button>)
    expect(getByText('Click me')).toBeInTheDocument()
  })

  // 使用自定义的匹配器断言 DOM 状态
  test('should be disabled when prop is set', () => {
    const { getByTestId } = render(
      <button disabled data-testid="button">
        Click me
      </button>
    )
    expect(getByTestId('button')).toBeDisabled()
  })

  // 模拟点击事件
  test('should call onClick when clicked', () => {
    const handleClick = jest.fn()
    const { getByText } = render(<button onClick={handleClick}>Click me</button>)

    fireEvent.click(getByText('Click me'))
    expect(handleClick).toHaveBeenCalled()
  })
})

接下来是业务组件:

// demo.tsx
import React from 'react'
import './index.scss'

interface Props {
  title: string
  showStar?: boolean
}

const Prefix = 'card-title'
export const CardTitle = (props: Props) => {
  const { title, showStar = true } = props

  return (
    <div className={`${Prefix}-title`}>
      {showStar && <span className={`${Prefix}-title-star`}>*</span>}
      <div>{title}</div>
    </div>
  )
}

// demo.test.tsx
import React from 'react'
import { render, screen } from '@testing-library/react'
import '@testing-library/jest-dom/extend-expect'

describe('CardTitle', () => {
  it('should have correct text content', () => {
    const { getByText } = render(<CardTitle title="测试标题" />)
    expect(getByText('测试标题')).toBeInTheDocument()
  })
  it('should render a span if showStar is true', () => {
    const { getByText } = render(<CardTitle title="test" showStar={true} />)
    expect(getByText('*')).toBeInTheDocument()
  })
  it('should not render a span if showStar is false', () => {
    render(<CardTitle title="测试标题" showStar={false} />)
    const span = screen.queryByText('*')
    expect(span).not.toBeInTheDocument()
  })
})

三、接口测试

在测试的时候我们常常希望: 把接口mock掉,不真正地发送请求到后端,自定义接口返回的值。

// api.ts(接口)
export const getUserRole = async () => {
  const result = await axios.post('XXX', { data: 'abc' })
  return result.data
}
// index.ts(调用函数)
export const getUserType = async () => {
  const result = await getUserRole()
  return result
}

1. Mock axios
这种方法可以在不同的测试用例中,根据我们的需要,来控制接口 data 的返回:

it('mock axios', async () => {
  jest.spyOn(axios, 'post').mockResolvedValueOnce({
    data: { userType: 'user' },
  })
  const { userType } = await getUserType()
  expect(userType).toBe('user')
})

2. Mock API
另一种方法是 Mock测试文件中的接口函数:

import * as userUtils from './api'

it('mock api', async () => {
  jest.spyOn(userUtils, 'getUserRole').mockResolvedValueOnce({ userType: 'user' })
  const { userType } = await getUserType()
  expect(userType).toBe('user')
})

3. Mock Http请求
我们可以不 Mock 任何函数实现,只对 Http 请求进行 Mock!先安装 msw:

🔧 msw 可以拦截指定的 Http 请求,有点类似 Mock.js,是做测试时一个非常强大好用的 Http Mock 工具。

npm install msw@latest --save-dev

需要说明一点,2.0.0以上的版本都是需要node>18的,由于不方便升级,我这里使用的是1.3.3版本(2024-03-15更新的,还是蛮新的哈)

如果你想在某个测试文件中想单独指定某个接口的 Mock 返回, 可以使用 server.use(mockHandler) 。

这里声明了一个 setup 函数,用于在每个用例前初始化 Http 请求的 Mock 返回。通过传不同值给 setup 就可以灵活模拟测试场景了。

import { rest } from 'msw'
import { setupServer } from 'msw/node'

describe('getUserType', () => {
  // 需要mock的接口地址
  const url = 'http://xxxx'
  const server = setupServer()
  const setup = (data: { userType: string }) => {
    server.use(
      rest.post(url, async (req, res, ctx) => {
        return res(ctx.status(200), ctx.json(data))
      })
    )
  }
  beforeAll(() => {
    server.listen()
  })

  afterEach(() => {
    server.resetHandlers()
  })

  afterAll(() => {
    server.close()
  })

  it('mock http', async () => {
    setup({ userType: 'user' })
    const { userType } = await getUserType()
    expect(userType).toBe('user')
  })
})

四、React Hook测试

如果我们需求中需要实现一个 Hook,那么我们要对 Hook 进行测试该怎么办呢?
🌰 举个例子:这里有一个useCounter,提供了增加、减少、设置和重置功能:

import { useState } from 'react'

export interface Options {
  min?: number
  max?: number
}

export type ValueParam = number | ((c: number) => number)

function useCounter(initialValue = 0) {
  const [current, setCurrent] = useState(initialValue)

  const setValue = (value: ValueParam) => {
    setCurrent((preValue) => (typeof value === 'number' ? value : value(preValue)))
  }
  // 增加
  const increase = (delta = 1) => {
    setValue((preValue) => preValue + delta)
  }
  // 减少
  const decrease = (delta = 1) => {
    setValue((preValue) => preValue - delta)
  }
  // 设置指定值
  const specifyValue = (value: ValueParam) => {
    setValue(value)
  }
  // 重置值
  const resetValue = () => {
    setValue(initialValue)
  }

  return [
    current,
    {
      increase,
      decrease,
      specifyValue,
      resetValue,
    },
  ] as const
}

export default useCounter

🙋有些同学会觉得 Hook 不就是纯函数么?为什么不能直接像纯函数那样去测呢?
❌ NoNoNo,React 规定 只有在组件中才能使用这些 Hooks,所以这样测试的结果就会得到下面的报错:
在这里插入图片描述

🙋那又有同学问了,我直接 Mock 掉这些 Hook 不就解决了?
❌ NoNoNo,假如除了 useState,还有 useEffect 这样的呢? 难道每个 React API 都要 Mock 一遍吗?

👉 这里循序渐进列举了三种方法,更推荐第三种哦~

1. 写组件进行整体测试

首先写一个组件,然后在组件内使用 useCounter,并把增加、减少、设置和重置功能绑定到按钮:

import React from 'react'
import useCounter from './useCounter'

export const UseCounterTest = () => {
  const [counter, { increase, decrease, specifyValue, resetValue }] = useCounter(0)
  return (
    <section>
      <div>Counter: {counter}</div>
      <button onClick={() => increase(1)}>点一下加一</button>
      <button onClick={() => decrease(1)}>点一下减一</button>
      <button onClick={() => specifyValue(10)}>点一下变成十</button>
      <button onClick={resetValue}>重置</button>
    </section>
  )
}

在每个用例中,我们通过点击按钮来模拟函数的调用,最后 expect 一下 Counter:n 的文本结果来完成测试:

import React from 'react'
import { describe, expect } from '@jest/globals'
import { render, fireEvent } from '@testing-library/react'
import '@testing-library/jest-dom/extend-expect'
import { UseCounterTest } from '.'

describe('useCounter', () => {
  it('可以做加法', async () => {
    const { getByText } = render(<UseCounterTest />)
    fireEvent.click(getByText('点一下加一'))
    expect(getByText('Counter: 1')).toBeInTheDocument()
  })

  it('可以做减法', async () => {
    const { getByText } = render(<UseCounterTest />)
    fireEvent.click(getByText('点一下减一'))
    expect(getByText('Counter: -1')).toBeInTheDocument()
  })

  it('可以设置值', async () => {
    const { getByText } = render(<UseCounterTest />)
    fireEvent.click(getByText('点一下变成十'))
    expect(getByText('Counter: 10')).toBeInTheDocument()
  })

  it('可以重置值', async () => {
    const { getByText } = render(<UseCounterTest />)
    fireEvent.click(getByText('点一下变成十'))
    fireEvent.click(getByText('重置'))
    expect(getByText('Counter: 0')).toBeInTheDocument()
  })
})

这个方法并不好,因为要用按钮来绑定一些操作并触发,可不可以直接操作函数呢?

2. 创建 setup 函数进行测试

我们不想一直和组件进行交互做测试,那么这个方法则只是借了 组件环境来生成一下 useCounter 结果, 用完就把别人抛弃了。

import React from 'react'
import { act, render } from '@testing-library/react'
import useCounter, { ValueParam } from '../useCounter'

interface UseCounterData {
  counter: number
  utils: {
    increase: (delta?: number) => void
    decrease: (delta?: number) => void
    specifyValue: (value: ValueParam) => void
    resetValue: () => void
  }
}

const setup = (initialNumber: number) => {
  const returnVal = {} as UseCounterData
  const UseCounterTest = () => {
    const [counter, utils] = useCounter(initialNumber)
    Object.assign(returnVal, {
      counter,
      utils,
    })
    return null
  }
  render(<UseCounterTest />)
  return returnVal
}

describe('useCounter', () => {
  it('可以做加法', async () => {
    const useCounterData: UseCounterData = setup(0)
    act(() => {
      useCounterData.utils.increase(1)
    })
    expect(useCounterData.counter).toEqual(1)
  })

  it('可以做减法', async () => {
    const useCounterData: UseCounterData = setup(0)
    act(() => {
      useCounterData.utils.decrease(1)
    })
    expect(useCounterData.counter).toEqual(-1)
  })

  it('可以设置值', async () => {
    const useCounterData: UseCounterData = setup(0)
    act(() => {
      useCounterData.utils.specifyValue(10)
    })
    expect(useCounterData.counter).toEqual(10)
  })

  it('可以重置值', async () => {
    const useCounterData: UseCounterData = setup(0)
    act(() => {
      useCounterData.utils.specifyValue(10)
      useCounterData.utils.resetValue()
    })
    expect(useCounterData.counter).toEqual(0)
  })
})

注意:由于setState 是一个异步逻辑,因此我们可以使用 @testing-library/react 提供的 act 里调用它。
act 可以确保回调里的异步逻辑走完再执行后续代码,详情可见官网这里

3. 使用 renderHook 测试
基于这样的想法,@testing-library/react-hooks 把上面的步骤封装成了一个公共函数 renderHook

注意:在 @testing-library/react@13.1.0 以上的版本已经把 renderHook 内置到里面了,这个版本需要和
react@18 一起使用。如果是旧版本,需要单独下载 @testing-library/react-hooks 包。

这里我使用新的版本,也就是内置的 renderHook:

import { act, renderHook } from '@testing-library/react'
import useCounter from '../useCounter'

describe('useCounter', () => {
  it('可以做加法', () => {
    const { result } = renderHook(() => useCounter(0))
    act(() => {
      result.current[1].increase(1)
    })
    expect(result.current[0]).toEqual(1)
  })

  it('可以做减法', () => {
    const { result } = renderHook(() => useCounter(0))
    act(() => {
      result.current[1].decrease(1)
    })
    expect(result.current[0]).toEqual(-1)
  })

  it('可以设置值', () => {
    const { result } = renderHook(() => useCounter(0))
    act(() => {
      result.current[1].specifyValue(10)
    })
    expect(result.current[0]).toEqual(10)
  })

  it('可以重置值', () => {
    const { result } = renderHook(() => useCounter(0))
    act(() => {
      result.current[1].specifyValue(10)
      result.current[1].resetValue()
    })
    expect(result.current[0]).toEqual(0)
  })
})

实际上 renderHook 只是 setup 方法里 setupTestComponent 的高度封装而已。

💥 其他的疑难杂症

如果测试组件和 React Router 做交互:

// useQuery.ts
import React from 'react'
import { useLocation } from 'react-router-dom'

// 获取查询参数
export const useQuery = () => {
  const { search } = useLocation()
  return React.useMemo(() => new URLSearchParams(search), [search])
}

// index.tsx
import React from 'react'
import { useQuery } from '../useQuery'

export const MyComponent = () => {
  const query = useQuery()
  return <div>{query.get('id')}</div>
}

使用 useLocation 时报错:
在这里插入图片描述

要创建 React Router 环境,我们可以使用 createMemoryHistory 这个 API:

import React from 'react'
import { useQuery } from '../useQuery'
import { createMemoryHistory, InitialEntry } from 'history'
import { render } from '@testing-library/react'
import { Router } from 'react-router-dom'

const setup = (initialEntries: InitialEntry[]) => {
  const history = createMemoryHistory({
    initialEntries,
  })

  const returnVal = {
    query: new URLSearchParams(),
  }

  const TestComponent = () => {
    const query = useQuery()
    Object.assign(returnVal, { query })
    return null
  }

  // 此处为 react router v6 的写法
  render(
    <Router location={history.location} navigator={history}>
      <TestComponent />
    </Router>
  )
  // 此处为 react router v5 的写法
  // render(
  //   <Router history={history}>
  //     <TestComponent />
  //   </Router>
  // );

  return returnVal
}

describe('userQuery', () => {
  it('可以获取参数', () => {
    const result = setup([
      {
        pathname: '/home',
        search: '?id=123',
      },
    ])
    expect(result.query.get('id')).toEqual('123')
  })

  it('查询参数为空时返回 Null', () => {
    const result = setup([
      {
        pathname: '/home',
      },
    ])
    expect(result.query.get('id')).toBeNull()
  })
})

另:好用的方法 🌟

1. test.only
使用场景:只想对单个测试用例进行调试时
在同一测试文件中,只有使用test.only的测试用例会被执行,其他测试用例则会被跳过。
举个例子🌰:(只有第二个测试用例会运行,第一个会被跳过,其他文件中的测试用例不会被跳过

describe('Example', () => {
  test('随便不知道是啥', () => {
    // 测试用例
  })
  test.only('我就举个例子', () => {
    // 测试用例
  })
})

2. test.skip
使用场景:想跳过某个测试用例进行调试时
在同一测试文件中,使用test.skip的测试用例会被跳过,其他测试用例正常执行。
用法同 test.only 我就不写例子了

还有好用的我再补充,散会~ 👏

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/525834.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

加州大学欧文分校英语基础语法专项课程02:Questions, Present Progressive and Future Tenses 学习笔记

Questions, Present Progressive and Future Tenses Course Certificate 本文是学习 Questions, Present Progressive and Future Tenses 这门课的学习笔记&#xff0c;如有侵权&#xff0c;请联系删除。 文章目录 Questions, Present Progressive and Future TensesWeek 01: …

大语言模型(LLM)为什么会产生幻觉?

一、幻觉的概念 在大语言模型&#xff08;LLM&#xff09;的语境之下&#xff0c;“幻觉”&#xff08;hallucination&#xff09;指的是模型在没有足够证据支持的情况下&#xff0c;生成的错误或虚构的信息。这种现象在自然语言处理&#xff08;NLP&#xff09;任务中尤其突出…

2024年MathorCup妈妈杯数学建模思路C题思路解析+参考成品

1 赛题思路 (赛题出来以后第一时间在群内分享&#xff0c;点击下方群名片即可加群) 2 比赛日期和时间 报名截止时间&#xff1a;2024年4月11日&#xff08;周四&#xff09;12:00 比赛开始时间&#xff1a;2024年4月12日&#xff08;周五&#xff09;8:00 比赛结束时间&…

jdk8新特性 方法引用

简介 lambda表达式是用来简化匿名内部类的方法引用 使用来简化 lambda表达式的 方法引用的标志 两个冒号 静态方法 静态方法 class CompareByAge {public static int compare(Student o1, Student o2) {return o1.getAge() - o2.getAge();} }静态方法引用 Arrays.sort(students…

游戏商业化活动通用测试点

备注:本文为博主原创文章,未经博主允许禁止转载。如有问题,欢迎指正。 个人笔记(整理不易,有帮助点个赞) 笔记目录:学习笔记目录_pytest和unittest、airtest_weixin_42717928的博客-CSDN博客 个人随笔:工作总结随笔_8、以前工作中都接触过哪些类型的测试文档-CSDN博客 …

谷歌(Google)技术面试——在线评估问题(三)

谷歌&#xff08;Google&#xff09;面试过程的第一步&#xff0c;你可能会收到一个在线评估链接。 评估有效期为 7 天&#xff0c;包含两个编码问题&#xff0c;需要在一小时内完成。 以下是一些供你练习的在线评估问题。 在本章结尾处&#xff0c;还提供了有关 Google 面试不…

免费分享一套微信小程序在线订餐(点餐)配送系统(SpringBoot+Vue),帅呆了~~

大家好&#xff0c;我是java1234_小锋老师&#xff0c;看到一个不错的微信小程序在线订餐(点餐)配送系统(SpringBootVue)&#xff0c;分享下哈。 项目视频演示 【免费】微信小程序在线订餐(点餐)配送系统(SpringBootVue) Java毕业设计_哔哩哔哩_bilibili【免费】微信小程序在…

配置 施耐德 modbusTCP 分布式IO子站 PRA0100

模块官方介绍&#xff1a;https://www.schneider-electric.cn/zh/product/BMXPRA0100 1. 总体步骤 2. 软件组态&#xff1a;在 Unity Pro 软件中创建编辑 PRA 模块工程 2.1 新建项目 模块箱硬件型号如下 点击 Unity Pro 软件左上方【新建】按钮&#xff0c;选择正确的 DIO …

【C语言】如何判断一个机器的大小端

如何判断一个机器的大小端 一&#xff1a;什么是机器的大小端二&#xff1a;为什么会有大小端三&#xff1a;设计一个小程序来判断当前机器的大小端方法一&#xff1a;指针类型强转方法二&#xff1a;联合体 一&#xff1a;什么是机器的大小端 机器的大小端是指在内存中存储多…

C++ | Leetcode C++题解之第13题罗马数字转整数

题目&#xff1a; 题解&#xff1a; class Solution { private:unordered_map<char, int> symbolValues {{I, 1},{V, 5},{X, 10},{L, 50},{C, 100},{D, 500},{M, 1000},};public:int romanToInt(string s) {int ans 0;int n s.length();for (int i 0; i < n; i) …

Python项目1 外星人入侵

武装飞船 1 规划项目 开发大型项目时&#xff0c;做好规划后再动手编写项目很重要。规划可确保你不偏离轨道&#xff0c;从而提高项目成功的可能性。 下面来编写有关游戏《外星人入侵》的描述&#xff0c;其中虽然没有涵盖这款游戏的所有细节&#xff0c;但能让你清楚地知道…

文心一言上线声音定制功能;通义千问开源模型;openAI又侵权?

文心一言上线定制专属声音功能 百度旗下 AI 聊天机器人文心一言上线新功能&#xff0c;用户录音一句话&#xff0c;即可定制声音。 使用这项功能需要使用文心一言 App。在创建智能体中&#xff0c;点击创建自己的声音&#xff0c;朗读系统提示的一句话&#xff0c;等候几秒钟时…

工程中实践的微服务设计模式

大家好&#xff0c;我是 方圆。最近在读《微服务架构设计模式》&#xff0c;开始的时候我非常的好奇&#xff0c;因为在我印象中&#xff0c;设计模式是常说的那23种设计模式&#xff0c;而微服务的设计模式又是什么呢&#xff1f;这个问题也留给大家&#xff0c;在文末我会附上…

synchronized锁机制升级过程——面试题

1. 无锁状态 对象在没有被任何线程锁定时处于无锁状态。此时对象头中的锁标志位通常表示为无锁&#xff08;例如&#xff0c;标记字段的特定位组合表示无锁或偏向锁状态&#xff09;。 2. 偏向锁&#xff08;Biased Locking&#xff09; 初次获取&#xff1a;当线程首次获得…

谷粒商城实战(011 业务-检索服务)

Java项目《谷粒商城》架构师级Java项目实战&#xff0c;对标阿里P6-P7&#xff0c;全网最强 总时长 104:45:00 共408P 此文章包含第173p-第p194的内容 介绍 这些过滤条件都可以写在must里&#xff0c;但是filter不参与评分&#xff0c;速度会快一些&#xff0c;所以一些分类等…

【三十六】【算法分析与设计】综合练习(3),39. 组合总和,784. 字母大小写全排列,526. 优美的排列

目录 39. 组合总和 对每一个位置进行枚举 枚举每一个数出现的次数 784. 字母大小写全排列 526. 优美的排列 结尾 39. 组合总和 给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target &#xff0c;找出 candidates 中可以使数字和为目标数 target 的 所有 不…

探索Python爬虫:解析网页数据的神奇之旅

在当今数字化时代&#xff0c;信息的获取变得比以往任何时候都更加便捷。然而&#xff0c;即使在互联网上&#xff0c;获取数据也需要通过正确的工具和技术。Python爬虫就是这样一种强大的工具&#xff0c;它可以让我们轻松地从互联网上收集数据&#xff0c;并将其转化为有用的…

学习人工智能:为何PyTorch深度学习框架不可或缺

在人工智能&#xff08;AI&#xff09;的浩瀚领域中&#xff0c;深度学习作为其核心分支&#xff0c;正以其强大的数据处理能力、模式识别能力和预测能力引领着科技的飞速发展。而在深度学习的众多工具与框架中&#xff0c;PyTorch无疑是一颗璀璨的明星。本文将从PyTorch的特点…

机器视觉学习(十二)—— 绘制图形

目录 一、绘制函数参数说明 1.1 cv2.line(&#xff09;绘制直线 1.2 cv2.rectangle&#xff08;&#xff09;绘制矩形 1.3 cv2.circle&#xff08;&#xff09; 绘制圆形 1.4 cv2.ellipse&#xff08;&#xff09;绘制椭圆 1.5 cv2.polylines&#xff08;&#xff09;绘制…

由elemnent-ui模拟一个全选、反选效果想到的购物车逻辑案例

本文参考 https://blog.csdn.net/sumimg/article/details/137508302?csdn_share_tail%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22137508302%22%2C%22source%22%3A%22sumimg%22%7D 我遇到的问题 点击店铺二级的时候&#xff0c;checkedCiti…