enzymejest TDD与BDD开发实战

一、前端自动化测试需要测什么

1. 函数的执行逻辑,对于给定的输入,输出是否符合预期。

2. 用户行为的响应逻辑。

-  对于单元测试而言,测试粒度较细,需要测试内部状态的变更与相应函数是否成功被调用。

-  对于集成测试而言,测试粒度较粗,一般测试ui展示上的变更(文本内容改变、组件类别改变等)。

3. 快照测试。对于不需要经常修改dom结构的组件,我们会存储一个快照,如果在后续的版本中修改了dom结构,测试用例会不通过,需要确认更新快照。

二、为什么需要自动化测试

你或许会疑惑,如果我们做的是一些业务的开发,而不是工具类函数的开发,似乎手动测试就可以满足需求。对于用户行为的响应逻辑可以通过点击来测试,dom结构的变更也可以通过肉眼观察。测试用例的代码可能甚至比业务代码量大,那前端有必要耗时耗力的进行自动化测试吗?

长期来看,集成自动化测试是有必要的。

1. 有利于回归测试。在公司项目中产品是经常迭代的,当我们修改了A功能,就需要测试相关联的B功能不受影响。如果没有自动化测试,每一次回归测试都需要手动进行,且并不能保证你没纳入考虑范围的C功能是不受影响的。自动化测试有利于降低回归测试的成本,并提高程序员的安全感。

2. 有利于代码重构。跟上一点类似,我们需要保证重构前后的预期是一致的,这时候我们就可以先对于老代码编写测试用例,使测试用例能够全部通过。再重构业务代码,如果重构后的代码也能通过全部的测试用例,那么代码的可靠性是较高的。如果采用手动测试的方案,那么你的执行流可能是:重构A功能->测试A功能->重构B功能->测试A和B功能。因为重构前的代码架构通常会混乱一点,所以为了确保后重构的功能不影响先重构的功能,手动测试的工作量会越来越大。

3. 有利于代码优化。当测试用例通过后就可以放心的进行代码优化了,省去了每次代码优化完手动测试的成本。

4. 前端开发与后端接口解耦。假设后端接口还未开发完成,当我们和后端约定好数据结构后,就可以模拟后端接口返回的数据并进行测试。当然,我们也可以选择在业务代码里写死数据并进行手动测试,但这就意味着后续需要修改业务代码;或者选择用抓包工具模拟响应结果进行手动测试,这种不需要修改业务代码,但是需要权衡手动测试和自动化测试的成本。

三、TDD与BDD

测试驱动开发(Test-Driven Development)的流程如下:

1. 根据要实现的功能编写测试用例,测试用例不通过

2. 实现相关功能,测试用例通过

3. 优化代码,完成开发

由于测试用例不通过时会显示红色,测试用例通过后会显示绿色,所以测试驱动开发又称Red-Green-Development。

测试驱动开发的优点如下:

1. 实现代码前先编写测试用例,确保代码一定是易于测试的,在开发视角的基础上扩展了测试视角,代码的组织架构会更好

2. 如果测试用例有误,可能在实现功能前后均可以通过测试。由于我们先编写测试用例后开发,如果测试用例在开发前就通过,那么大概率测试用例是有问题的,我们就可以及时发现修改。降低了编写出错误测试代码的可能性。

3. 自动化测试的通用优点。

行为驱动开发(Behavior-Driven Development)一般是测试驱动开发的自然延伸,它的核心在于根据用户行为来设计测试用例,对于是否需要在开发前编写测试用例没有强制要求。

四、单元测试与集成测试

单元测试的优点: 测试粒度细,代码覆盖率高,运行速度快。

单元测试的缺点

1. 代码量大 。

2. 关注代码实现细节,与业务代码耦合度高。

3. 每个单元的单元测试通过,也无法保证集成后能正常运行。比如说A组件给B组件传入data,A组件测试传入的data结构为对象,B组件测试接收到的data为数组,两个组建的测试用例都能通过,但是集成后运行就会由于数据结构不一致报错。

单元测试的适用场景:工具库。

集成测试的优点

1. 测试粒度没那么细,比所有单元的单元测试代码量总和小。

2. 不关注代码实现细节,只关心展示给用户的结果,业务代码耦合度较低。

3. 集成测试能确保单元能够协作运行,通过集成测试通常来讲系统对于用户能够正常运行。

集成测试的缺点

1. 集成测试测试可能不如单元测试细致。

2. 集成测试需要运行多个组件,测试速度会慢一些。

集成测试的适用场景:业务系统。

五、具体实现

单元测试、集成测试、TDD、BDD之间要怎么集成其实没有标准答案,而且BDD和TDD本身也不是对立的概念。只是BDD本身是以用户的“故事”为导向的,这些故事通常涉及不止一个单元所以BDD通常和集成测试结合,单元测试与TDD结合。

1. 单元测试与TDD

用react脚手架创建出项目后,由于enzyme官方没有适配react17及以上版本,需要将react版本降级为16。如果需要用react17及以上的版本,可以用非官方的适配器,或者改用react-testing-library。但是react-testing-library本身不关注代码实现细节,而是以用户视角触发的,所以我个人感觉不是很适合做单元测试。

npm install react@16 react-dom@16 --save
npm install enzyme enzyme-adapter-react-16 --save-dev

 我们做一个简单的todoList项目,当我们在输入框中输入内容,按下回车后就会展现在下方。点击项尾的删除键可以进行删除。

我们将这个项目拆分为Header组件和UndoList组件。

我们在src目录下创建如下结构,__tests__/unit目录下编写单元测试代码,TodoList目录下编写业务代码。

因为采用TDD的模式开发,所以先来编写测试代码。

环境准备

enzyme与react集成需要配置适配器,由于这个配置需要在每个测试文件最开始引入,所以我们可以将其抽离到一个单独的文件中,然后对jest进行配置,让jest在测试环境准备好后执行该文件。

react内部其实已经对jest进行了配置,我们需要做的就是将其暴露出来。

// npm run eject的执行前提是没有未追踪的文件,所以我们需要先初始化git仓库并提交
git init
git add .
git commit -m "init resposity"
npm run eject

 在运行完npm run eject后,我们可以发现package.json文件有新增的配置项Jest,配置项中有一个属性是setupFilesAfterEnv。

  "jest": {
    // ...
    "setupFilesAfterEnv": [
      "<rootDir>/src/setupTests.js"
    ],
    // ...
  },

 可以看出,setupTests文件会在测试环境准备好后执行,所以我们只需要新增文件setupEnzyme.js,修改jest配置项,并将enzyme的适配器配置填入该文件即可。

  "jest": {
    // ...
    "setupFilesAfterEnv": [
      "<rootDir>/src/setupTests.js",
      "<rootDir>/src/setupEnzyme.js"
    ],
    // ...
  },
// src/setupEnzyme.js

import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

Enzyme.configure({ adapter: new Adapter() });

由于jest本身不支持TextEncoder、TextDecoder、ReadableStream,而在enzyme内部又会调用到相应的方法,所以我们需要在import Enzyme前将方法挂载在global上。

// setupTests.js

import { TextEncoder, TextDecoder, ReadableStream } from 'util';
global.TextEncoder = TextEncoder;
global.TextDecoder = TextDecoder;
global.ReadableStream = ReadableStream;

测试用例编写

- Header

header组件的功能是点击回车键时,能将数据传送给TodoList,我们将header组件设计成受控组件(即组件内的状态会根据用户输入实时更新)。对功能进行拆解如下:

1. 输入框的展示值为state.inputData,state.inputData初始化为空。

2. input框输入内容时,state.inputData随之改变。

3. 当输入框不为空时,用户敲击回车后,调用props.addUndoItem,state.inputData清空。

4. 当输入框为空时,用户敲击回车后,不调用props.addUndoItem.

4. 快照测试。

test('输入框的展示值为state,state.inputData初始化为空', () => {
  const wrapper = shallow(<Header />);
  const input = wrapper.find("[data-test-id='input']");
  expect(input.prop('value')).toBe(wrapper.state('inputData'));
  expect(wrapper.state('inputData')).toBe('');
})

由于我们还未编写业务代码,运行npx jest时,测试用例是不通过的。

业务代码编写

由于enzyme只能追踪到类组件里的状态,所以这里我们创建类组件。

如果你需要用函数组件,可以参考Testing React Hook State Changes - DEV Community,他的核心思路是相信react,只要我们调用了setState方法,传入了正确的参数,就认为状态可以被正确的修改。通过mock setState方法,来判断调用了函数并传入了正确的参数。

import React, { Component } from "react";

export default class Header extends Component {
  state = {
    inputData: "",
  };
  render() {
    return (
      <div>
        <input data-test-id="input" value={this.state.inputData} />
      </div>
    );
  }
}

再次运行测试,测试通过。

 然后是第2-4个测试用例及业务代码。

test('输入框输入字符时,state.inputData随之改变', () => {
  const wrapper = shallow(<Header />);
  const input = wrapper.find("[data-test-id='input']");
  const inputData = "hello world";
  input.simulate('change', { target: { value: inputData } });
  expect(wrapper.state('inputData')).toBe(inputData);
})
import React, { Component } from "react";

export default class Header extends Component {
  state = {
    inputData: "",
  };
  render() {
    return (
      <div>
        <input
          data-test-id="input"
          value={this.state.inputData}
          onChange={(e) => this.setState({ inputData: e.target.value })}
        />
      </div>
    );
  }
}
test('当输入框不为空时,用户敲击回车后,调用props.addUndoItem,state.inputData清空', () => {
  const func = jest.fn();
  const wrapper = shallow(<Header addUndoItem={func} />);
  const inputData = "hello world";
  wrapper.setState({ inputData });
  const input = wrapper.find("[data-test-id='input']");
  input.simulate('keyUp', { keyCode: 13 });
  expect(func).toHaveBeenCalled();
  expect(func).toHaveBeenLastCalledWith(inputData);
  expect(wrapper.state('inputData')).toBe('');
})

test('当输入框为空时,用户敲击回车后,调用props.addUndoItem,state.inputData清空', () => {
  const func = jest.fn();
  const wrapper = shallow(<Header addUndoItem={func} />);
  wrapper.setState({ inputData: '' });
  const input = wrapper.find("[data-test-id='input']");
  input.simulate('keyUp', { keyCode: 13 });
  expect(func).not.toHaveBeenCalled();
})
import React, { Component } from "react";

export default class Header extends Component {
  state = {
    inputData: "",
  };
  handleKeyUp = (e) => {
    if (e.keyCode === 13 && this.state.inputData !== "") {
      this.props.addUndoItem(this.state.inputData);
      this.setState({ inputData: "" });
    }
  };
  render() {
    return (
      <div>
        <input
          data-test-id="input"
          value={this.state.inputData}
          onChange={(e) => this.setState({ inputData: e.target.value })}
          onKeyUp={this.handleKeyUp}
        />
      </div>
    );
  }
}

header组件的逻辑编写完成,接下来我们补充完样式以后,就可以进行快照测试了。

先引入一下header。

// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'))

// App.jsx
import TodoList from './container/TodoList';
export default function App() {
  return <TodoList />
}

// container/TodoList/index.jsx
import React, { Component } from "react";
import Header from "./Header";
export default class index extends Component {
  render() {
    return (
      <div>
        <Header />
      </div>
    );
  }
}

运行npm run start,页面展示如下。

我们对样式进行优化,优化后页面展示如下。

// container/TodoList/style.css

.header-wrapper {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100px;
  background-color: #e1d3d3;
  gap: 20px;
}

.header-input {
  outline: none;
  line-height: 24px;
  width: 360px;
  border-radius: 5px;
  text-indent: 10px;
}

.header-span {
  font-size: 24px;
  font-weight: bold;
}
// container/TodoList/Header.jsx
import React, { Component } from "react";

export default class Header extends Component {
  state = {
    inputData: "",
  };
  handleKeyUp = (e) => {
    if (e.keyCode === 13 && this.state.inputData !== "") {
      this.props.addUndoItem(this.state.inputData);
      this.setState({ inputData: "" });
    }
  };
  render() {
    return (
      <div className="header-wrapper">
        <span className="header-span">TodoList</span>
        <input
          data-test-id="input"
          className="header-input"
          value={this.state.inputData}
          placeholder="请输入待办项"
          onChange={(e) => this.setState({ inputData: e.target.value })}
          onKeyUp={this.handleKeyUp}
        />
      </div>
    );
  }
}
// container/TodoList/index.jsx
import "./style.css";
// ...
 快照测试
test('快照测试', () => {
  const wrapper = shallow(<Header />);
  expect(wrapper).toMatchSnapshot();
})

 运行测试用例后,会生成__snapshot__文件夹,后续修改样式/dom结构都会导致测试不通过。

剩下两个组件的流程不做赘述,编写完代码如下。

// __tests__/TodoList.js

import { shallow } from "enzyme";
import TodoList from '../../index';

let wrapper;
beforeEach(() => {
  wrapper = shallow(<TodoList />);
})

test('快照测试', () => {
  expect(wrapper).toMatchSnapshot();
})

test('state.undoList初始化为空', () => {
  expect(wrapper.state('undoList')).toEqual([]);
})

test('向header传入addUndoItem方法,当该方法被调用时,更新state.undoList', () => {
  const header = wrapper.find("[data-test-id='header']");
  expect(header.prop('addUndoItem')).toBeTruthy();
  expect(header.prop('addUndoItem')).toEqual(wrapper.instance().addUndoItem);
  const prevUndoList = ['hello'];
  wrapper.setState({ undoList: prevUndoList });
  const inputData = 'world';
  wrapper.instance().addUndoItem(inputData);
  expect(wrapper.state('undoList')).toEqual([...prevUndoList, inputData]);
})

test('向undoList组件传入list属性,属性值为state.undoList', () => {
  const undoList = wrapper.find("[data-test-id='undo-list']");
  expect(undoList.prop('list')).toBeTruthy();
  expect(undoList.prop('list')).toEqual(wrapper.state('undoList'));
})

test('向undoList组件传入deleteUndoItem方法,当该方法被调用时,更新state.undoList', () => {
  const undoListData = ['hello', 'world'];
  wrapper.setState({ undoList: [...undoListData] });
  const undoList = wrapper.find("[data-test-id='undo-list']");
  expect(undoList.prop('deleteUndoItem')).toBeTruthy();
  expect(undoList.prop('deleteUndoItem')).toEqual(wrapper.instance().deleteUndoItem);
  wrapper.instance().deleteUndoItem(0);
  expect(wrapper.state('undoList')).toEqual([undoListData[1]]);
})
// TodoList/index.js
import React, { Component } from "react";
import Header from "./Header";
import "./style.css";
import UndoList from "./UndoList";

export default class index extends Component {
  state = {
    undoList: [],
  };
  addUndoItem = (item) => {
    this.setState({
      undoList: [...this.state.undoList, item],
    });
  };
  deleteUndoItem = (index) => {
    const newUndoList = this.state.undoList;
    newUndoList.splice(index, 1);
    this.setState({ undoList: newUndoList });
  };
  render() {
    return (
      <div>
        <Header data-test-id="header" addUndoItem={this.addUndoItem} />
        <UndoList
          data-test-id="undo-list"
          list={this.state.undoList}
          deleteUndoItem={this.deleteUndoItem}
        />
      </div>
    );
  }
}
// __tests__/Undolist.js
import { shallow } from "enzyme";
import UndoList from "../../UndoList";

test('快照测试', () => {
  const wrapper = shallow(<UndoList list={[]} />);
  expect(wrapper).toMatchSnapshot();
})

test('props.list为空时,列表展示为空', () => {
  const wrapper = shallow(<UndoList list={[]} />);
  // console.log(wrapper.find("[data-test-id='list-item']"));
  expect(wrapper.find("[data-test-id='list-item']").length).toBe(0);
})

test('props.list为不为空时,列表展示对应项', () => {
  const list = ['hello', 'world'];
  const wrapper = shallow(<UndoList list={list} />);
  const listItem = wrapper.find("[data-test-id='list-item']");
  expect(listItem.length).toBe(2);
  expect(listItem.at(0).text()).toBe('hello');
  expect(listItem.at(1).text()).toBe('world');
})

test('点击删除按钮时,调用props.deleteUndoItem', () => {
  const list = ['hello', 'world'];
  const func = jest.fn();
  const wrapper = shallow(<UndoList list={list} deleteUndoItem={func} />);
  const deleteBtn = wrapper.find("[data-test-id='delete-btn']");
  deleteBtn.at(0).simulate('click');
  expect(func).toHaveBeenCalled();
  expect(func).toHaveBeenLastCalledWith(0);
})
// UndoList.jsx
import React, { Component } from "react";

export default class UndoList extends Component {
  render() {
    return (
      <ul className="undo-list-wrapper">
        {this.props.list.map((item, index) => {
          return (
            <li key={index} className="undo-list-item">
              <div data-test-id="list-item">{item}</div>
              <div
                data-test-id="delete-btn"
                className="undo-delete-btn"
                onClick={() => this.props.deleteUndoItem(index)}
              >
                -
              </div>
            </li>
          );
        })}
      </ul>
    );
  }
}

页面展示效果如下

我们运行npx jest --coverage来看一下测试覆盖率。

运行完命令后会新生成coverage文件夹,我们在浏览器打开index.html。

由于我们没给src/index.js和src/App.jsx编写测试用例,所以第一条显示为0。但是对于我们编写了测试用例的TodoList组件,可以看到测试的覆盖率是百分百,所以TDD这种开发模式的代码覆盖率是非常高的。

2. 集成测试与BDD

可以看出,在进行单元测试时,测试代码量是比较大的,如果单元内部的逻辑很复杂,那么测试代码量还会大幅增加。

而且,我们在单元测试中用了大量业务代码内的属性,像state、props等,这些其实对于用户来说是不可见的。那么,我们可不可以站在用户视角,以模拟用户行为的方式来进行黑盒测试呢?答案是肯定的。

站在用户角度,Todolist无非就干了三件事。

1. 待办项初始化为空。

2. 输入待办项后回车,待办项会被展示在最下方。

3. 点击删除按钮,对应的待办项被删除。

根据以上的用户故事,我们编写对应的测试代码。

import { mount } from 'enzyme';
import TodoList from '../../index';

let wrapper;

// 对于集成测试而言,我们需要渲染子组件,所以调用mount方法
// 对于mount方法,元素会被真正挂载在页面上,所以如果我们在两个测试用例里面分别创建了wrapper
// 且每个wrapper中有一个undoListItem
// 那么在不调用卸载方法的情况下,页面上会存在两个undoListItem
// 所以在集成测试里,我只创建了一次wrapper
beforeAll(() => {
  wrapper = mount(<TodoList />);
});

test(`
  1. 用户进入网站
  2. 待办项显示为空
  `, () => {
  const undoListItem = wrapper.find("[data-test-id='list-item']");
  expect(undoListItem.length).toBe(0);
})

test(`
  1. 用户输入待办项
  2. 用户敲击回车
  3. 待办项展示在下方
  `, () => {
  const input = wrapper.find("[data-test-id='input']");
  const inputData = "hello";
  input.simulate('change', { target: { value: inputData } });
  input.simulate('keyUp', { keyCode: 13 });
  const undoListItem = wrapper.find("[data-test-id='list-item']")
  expect(undoListItem.length).toBe(1);
  expect(undoListItem.text()).toBe(inputData);
});

test(`
  1. 用户输入待办项
  2. 用户敲击回车
  3. 在原待办项下方新增待办项
  `, () => {
  const input = wrapper.find("[data-test-id='input']");
  const inputData = "world";
  input.simulate('change', { target: { value: inputData } });
  input.simulate('keyUp', { keyCode: 13 });
  const undoListItem = wrapper.find("[data-test-id='list-item']");
  expect(undoListItem.length).toBe(2);
  expect(undoListItem.at(1).text()).toBe(inputData);
});

test(`
  1. 用户点击第一项的删除按钮
  2. 第一项被删除
  `, () => {
  const deleteBtn = wrapper.find("[data-test-id='delete-btn']");
  deleteBtn.at(0).simulate('click');
  const undoListItem = wrapper.find("[data-test-id='list-item']");
  expect(undoListItem.length).toBe(1);
  expect(undoListItem.text()).toBe("world");
})

test(`
  1. 用户点击第一项的删除按钮
  2. 第一项被删除
  `, () => {
  const deleteBtn = wrapper.find("[data-test-id='delete-btn']");
  deleteBtn.at(0).simulate('click');
  const undoListItem = wrapper.find("[data-test-id='list-item']");
  expect(undoListItem.length).toBe(0);
})

因为我们已经实现了对应的业务代码,所以测试用例均可以正常通过。

可以看出,相比较对于一个个单元进行单元测试,整体编写集成测试的代码量是会更少的。如果后续我们修改了state里的数据结构,或者是props的属性名,只要最终展现在页面上的结果不变,那么集成测试都可以通过。

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

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

相关文章

Flutter项目开发模版,开箱即用(Plus版本)

前言 当前案例 Flutter SDK版本&#xff1a;3.22.2 本文&#xff0c;是由这两篇文章 结合产出&#xff0c;所以非常建议大家&#xff0c;先看完这两篇&#xff1a; Flutter项目开发模版&#xff1a; 主要内容&#xff1a;MVVM设计模式及内存泄漏处理&#xff0c;涉及Model、…

Spring Boot - 日志功能深度解析与实践指南

文章目录 概述1. Spring Boot 日志功能概述2. 默认日志框架&#xff1a;LogbackLogback 的核心组件Logback 的配置文件 3. 日志级别及其配置配置日志级别3.1 配置文件3.2 环境变量3.3 命令行参数 4. 日志格式自定义自定义日志格式 5. 日志文件输出6. 日志归档与清理7. 自定义日…

IWOA-GRU和GRU时间序列预测(改进的鲸鱼算法优化门控循环单元)

时序预测 | MATLAB实现IWOA-GRU和GRU时间序列预测(改进的鲸鱼算法优化门控循环单元) 目录 时序预测 | MATLAB实现IWOA-GRU和GRU时间序列预测(改进的鲸鱼算法优化门控循环单元)预测效果基本介绍模型描述程序设计参考资料 预测效果 基本介绍 MATLAB实现IWOA-GRU和GRU时间序列预测…

【SpringBoot】日志处理-异常日志(Logback)

文章目录 异常日志&#xff08;Logback&#xff09;1、将 logback-spring.xml 文件放入项目的 src/main/resources 目录下2、配置 application.yml 文件3、使用 Logback 记录日志 异常日志&#xff08;Logback&#xff09; 使用 Logback 作为日志框架时&#xff0c;可以通过配…

【RK3568笔记】Android修改开机动画

概述 Android 的开机动画是由一系列连续的 PNG 图片作为帧组成的动画形式&#xff0c;不是一张 GIF 图片。将各帧 PNG 图片以压缩方式进行保存&#xff08;压缩方式要求是存储压缩&#xff09;&#xff0c;并将保存的文件名命名为 bootanimation.zip&#xff0c;这个 bootanim…

复合机器人助力手机壳cnc加工向自动化升级

在当今竞争激烈的制造业领域&#xff0c;如何提高生产效率、降低成本、提升产品质量&#xff0c;成为众多企业面临的关键挑战。尤其是在手机壳 CNC 加工这一细分行业&#xff0c;随着市场需求的持续增长&#xff0c;对生产效能的要求愈发严苛。而复合机器人的出现&#xff0c;正…

HTML——75. 内联框架

<!DOCTYPE html> <html><head><meta charset"UTF-8"><title>内联框架</title><style type"text/css">iframe{width: 100%;height: 500px;}</style></head><body><!--iframe元素会创建包含…

(七)人工智能进阶之人脸识别:从刷脸支付到智能安防的奥秘,小白都可以入手的MTCNN+Arcface网络

零、开篇趣谈 还记得第一次用支付宝"刷脸"时的新奇感吗&#xff1f;或者被抖音的人脸特效逗乐的瞬间&#xff1f;这些有趣的应用背后&#xff0c;其实藏着一个精妙的AI世界。今天&#xff0c;就让我们开启一段奇妙的人脸识别技术探索之旅吧&#xff01; 一、人脸识…

1. 使用springboot做一个音乐播放器软件项目【前期规划】

背景&#xff1a; 现在大部分音乐软件都是要冲会员才可以无限常听的。对于喜欢听音乐的小伙伴&#xff0c;资金又比较紧张&#xff0c;是那么的不友好。作为程序员的我&#xff0c;也是喜欢听着歌&#xff0c;敲着代码。 最近就想做一个音乐播放器的软件&#xff0c;在内网中使…

STM32-笔记37-吸烟室管控系统项目

一、项目需求 1. 使用 mq-2 获取环境烟雾值&#xff0c;并显示在 LCD1602 上&#xff1b; 2. 按键修改阈值&#xff0c;并显示在 LCD1602 上&#xff1b; 3. 烟雾值超过阈值时&#xff0c;蜂鸣器长响&#xff0c;风扇打开&#xff1b;烟雾值小于阈值时&#xff0c;蜂鸣器不响…

【Linux】记录一下考RHCE的学习过程(七)

年底了&#xff0c;公司接的北京地铁轨道交通的项目做不完了&#xff0c;一百多列地铁的设备都得调&#xff0c;派我出差了几周&#xff0c;这几天才回来&#xff0c;出差累死了实在是没办法更新。&#xff08;YOASOBI的二开票还没抢到ToT&#xff0c;哭死&#xff0c;看看回滚…

[读书日志]从零开始学习Chisel 第六篇:Scala面向对象编程——特质(敏捷硬件开发语言Chisel与数字系统设计)

3.4特质 3.4.1什么是特质 特质使用trait开头&#xff0c;它与单例对象很像&#xff0c;两者都不能有输入参数&#xff0c;但单例对象是具体的&#xff0c;特质是抽象的。两者都不能用new实例化&#xff0c;类&#xff0c;单例对象&#xff0c;特质三者内部都可以包含字段和方…

VuePress2配置unocss的闭坑指南

文章目录 1. 安装依赖&#xff1a;准备魔法材料2. 检查依赖版本一定要一致&#xff1a;确保魔法配方准确无误3. 新建uno.config.js&#xff1a;编写咒语书4. 配置config.js和client.js&#xff1a;完成仪式 1. 安装依赖&#xff1a;准备魔法材料 在开始我们的前端魔法之前&…

游戏引擎学习第77天

仓库: https://gitee.com/mrxiao_com/2d_game 回顾昨天的 bug 今天我们继续开发进度&#xff0c;进行调试昨天代码的问题&#xff0c;主要是关于如何跟踪玩家和敌人在世界中的高度位置。虽然我们做的是一款 2D 游戏&#xff0c;但我们希望能够处理多层的房间&#xff0c;玩家…

uniapp结合movable-area与movable-view实现拖拽功能2

前言 上篇我们写了&#xff0c;固定高度的拖拽&#xff0c;这篇我们将进行不固定高度的拖拽模块编写完成。 开始 一、初始化 我们在list数组里面增加一个data的动态数组&#xff0c;这样可以动态改变元素的高度。 当前元素y 上一个元素的高度 <template><view s…

ubuntu为Docker配置代理

终端代理 我们平常在ubuntu终端中使用curl或git命令时&#xff0c;往往会很慢。 所以&#xff0c;首先需要给ubuntu终端环境添加代理。 查看自身那个软件的端口号&#xff0c;我这里是7890。 sudo gedit ~/.bashrcexport http_proxyhttp://localhost:7890 export https_pr…

【Uniapp-Vue3】v-if条件渲染及v-show的选择对比

如果我们想让元素根据响应式变量的值进行显示或隐藏可以使用v-if或v-show 一、v-show 另一种控制显示的方法就是使用v-show&#xff0c;使用方法和v-if一样&#xff0c;为true显示&#xff0c;为false则不显示。 二、v-if v-if除了可以像v-show一样单独使用外&#xff0c;还…

wujie无界微前端框架初使用

先说一下项目需求&#xff1a;将单独的四套系统的登录操作统一放在一个入口页面进行登录&#xff0c;所有系统都使用的是vue3&#xff0c;&#xff08;不要问我为啥会这样设计&#xff0c;产品说的客户要求&#xff09; 1.主系统下载wujie 我全套都是vue3&#xff0c;所以直接…

C语言 数组编程练习

1.将数组A的内容和数组B中的内容进行交换。&#xff08;数组一样大&#xff09; 2.创建一个整形数组&#xff0c;完成对数组的操作 实现函数Init()初始化数组全为0 实现print()打印数组的每个元素 实现reverse()函数完成数组元素的逆置 //2.创建一个整形数组&#xff0c;完…

【three.js】模型-几何体Geometry,材质Material

模型 在现实开发中&#xff0c;有时除了需要用代码创建模型之外&#xff0c;多数场景需要加载设计师提供的使用设计软件导出的模型。此时就需要使用模型加载器去加载模型&#xff0c;不同格式的模型需要引入对应的模型加载器&#xff0c;虽然加载器不同&#xff0c;但是使用方式…