当前位置: 首页 > news >正文

本地电脑做服务器建网站吾爱上云小程序制作

本地电脑做服务器建网站,吾爱上云小程序制作,wordpress承载的数据,创建一个公司需要多少钱前端自动化测试#xff1a;Testing Library 篇 引言 前端测试 静态测试 eslint、TypeScript 单元测试 jest、mocha 集成测试 enzyme、react-testing-library、mock 爬虫 前后端解耦 为什么要引入自动化测试 测试可以让开发者站在用户的角度考虑问题#xff0c;通过测试的手…前端自动化测试Testing Library 篇 引言 前端测试 静态测试 eslint、TypeScript 单元测试 jest、mocha 集成测试 enzyme、react-testing-library、mock 爬虫 前后端解耦 为什么要引入自动化测试 测试可以让开发者站在用户的角度考虑问题通过测试的手段确保组件的每个功能都可以正常地运行。 在编写单元测试时很大情况下会对组件代码进行反复的调整通过不断的打磨避免了开发时考虑不周到的情况从而提高组件的质量。对于变动不频繁的业务模块也是同样的道理。 根据《Google 软件测试之道》一书中对于测试认证级别的定义如下 级别 1 使用测试覆盖率工具。使用持续集成。测试分级为小型、中型、大型。创建冒烟测试集合主流程测试用例。标记哪些测试是非确定性的测试测试结果不唯一。 级别 2 如果有测试运行结果为红色失败就不会发布。每次代码提交之前都要求通过冒烟测试。自测简单走下主流程各种类型的整体代码覆盖率要大于50%。小型测试的覆盖率要大于10%。 级别 3 所有重要的代码变更都要经过测试。小型测试的覆盖率大于50%。新增重要功能都要通过集成测试的验证。 级别 4 在提交任何新代码之前都会自动运行冒烟测试。冒烟测试必须在30分钟内运行完毕。没有不确定性的测试。总体测试覆盖率应该不小于40%。小型测试的代码覆盖率应该不小于25%。所有重要的功能都应该被集成测试验证到。 级别 5 对每一个重要的缺陷修复都要增加一个测试用例与之对应。 积极使用可用的代码分析工具。 总体测试覆盖率不低于60%。 小型测试代码覆盖率应该不小于40%。 测试工具的选择 JestMochaTesting Library 和 Enzyme 是前端自动化测试使用较广泛的工具。 可遵循的简单规则 AAA 模式编排Arrange、执行Act、断言Assert 一、编排 编排阶段可分为两部分。渲染组件 和 获取所需要的 DOM 元素。 二、执行 当把需要测试的内容准备好了后便可以借助测试库的工具 API 执行模拟操作了。如使用 React Testing Library 的 fireEvent 模拟点击事件。这一步非必须的也可以直接对编排获取到的内容进行断言 fireEvent.click(mockFn)三、断言 断言也即是判断。通过 Jest 或 React Testing Library 内置的断言语句对上述内容进行判断通过则 pass。 简介 官网手册 Testing Library 是一个轻量级的测试解决方案用于通过查询和与 DOM 节点交互无论是用 JsDOM / Jest 模拟还是在浏览器中来测试网页。提供贴近于用户在页面中查找元素的方式查询节点。 Testing Library 可应用于各个主流框架包括 React、Angular 和 Vue并且可作为核心在 Cypress 测试框架中使用。 本文探究 React Testing LibraryRTL 思想 Testing Library 鼓励测试避免 实现细节比如组件的内部状态、内部方法、生命周期、子组件而强调关注与用户实际交互相似的内容。 编写的测试与软件用户的使用方式越相似它们越能给测试带来信心。 核心 查询 类型 在 RTL 中提供三种方式查询元素分别是getByqueryByfindBy。如果要查询多元素使用xxxAllBy 代替。差异如下 查询类型0 对应1 对应1 对应异步/等待 awaitgetBy…报错返回报错noqueryBy…null返回报错nofindBy…报错返回报错yesgetAllBy…报错返回数组返回数组noqueryAllBy…[ ]返回数组返回数组nofindAllBy…报错返回数组返回数组yes通常来说getBy 用于查询正常存在的元素找不到报错queryBy 用于查询希望不存在的元素找不到不报错findBy 则用于查询需要等待的异步元素。 使用 *ByRole 会将隐藏元素名称读取为 “”。可考虑更好另一种查询方法如仍需使用 *ByRole 进行查询可参考官方手册。 问题详情 优先级 按照测试库的指导思想在使用查询 API 时应该遵守一定的优先级站在用户的使用角度能直接用从页面上看到的作为选择器便不使用用户看不到的 id、class 等选择器。 常规的 getByRole 查找具有特定角色的元素 getByLabelText 查找具有给定文本匹配的 label 元素 getByPlaceholderText 查找具有占位符属性的元素 getByText 查找具有文本节点的元素 getByDisplayValue 查找具有 value 的控件元素 语义查询 getByAltText 查找具有 alt 属性alt 对应的 text 文本匹配的元素 getByTitle 返回具有 title 属性title 对应的 text 文本匹配的元素 借助测试 Id getByTestId考虑在生产环境中避免无意义的属性可以借助 babel-plugin-react-remove-properties 去除 data-test 测试辅助选择器 // .bablerc {env: {production: {plugins: [react-remove-properties]}} }实践 由于 Test Library 强调站在用户的角度进行测试因此更偏向使用 DOM 上的 Accessibility 的元素。但有时候只能用样式选择器或直接使用样式选择器更有效率只能说找到哲学与生活的平衡点就好 查看 HTML 元素对应的 ARIA role 方法有以下几点 一、借助 Chrome 开发者工具 元素(Elements) 无障碍功能(Accessibility) 二、借助 RTL 提供的 logRoles API import { render, logRoles } from testing-library/react;test(find ARIA role, () {const { container } render(Component /);logRoles(container); })三、借助第三方插件 使用谷歌浏览器的插件可以更为简易地获取元素。 Testing library: which query Testing Playground 用户操作 借助 fireEvent 可以模拟实际用户产生的交互事件。 Testing Library 下还有一个高级库testing-library/user-event 其提供了比 fireEvent 更多的交互事件。 fireEvent(node: HTMLElement, event: Event)fireEvent 对应的 eventMap 事件集属性 常用断言 RTL 扩展了 jest 的 api定义了自己的断言函数所有的断言函数包含在testing-library/jest-dom包中。详见内置断言库 toBeDisabled toBeEnabled toBeEmptyDOMElement toBeInTheDocument toBeInvalid toBeRequired toBeValid toBeVisible toContainElement toContainHTML toHaveAttribute toHaveClass toHaveFocus toHaveFormValues toHaveStyle toHaveTextContent toHaveValue toHaveDisplayValue toBeChecked toBePartiallyChecked toHaveDescriptionJest 使用 RTL 需要搭配 Jest 所以我们先来 “荒腔走板” 地对 Jest 基础使用进行一个简单的学习。 介绍 Jest是一个测试框架RTL 是一个测试解决方案。RTL 存在的意义是通过更优美、功能更强大的方式去完成测试的编写。 断言匹配器 判断真假 toBeNull toBeUndefined toBeDefined toBeTruthy toBeFalsy 数字相关 toBeGeaterThan 大于某个数 toBeGeaterThanOrEqual 大于或等于 toBeLessThan 小于某数 toBeLessThanOrEqual 小于或等于 toBeCloseTo 浮点数的相等判断 not 修饰符 与期望相反的匹配 字符串匹配 toMatch 数组集合相关 toContain 判断数组或集合是否包含某个元素 toHaveLength 判断数组的长度 函数相关 toHaveBeenCalled 判断 mockFunc 是否被调用 toHaveBeenCalledWith(‘_test_param’) 调用接收的参数是否为 _test_param toHaveBeenCalledTimes(2) 调用的次数 toHaveReturned 是否有返回值 toHaveReturnedWith(‘_test_return’) 返回值是否为 _test_return 异常相关 toThrow 判断抛出的异常是否符合预期 Mock 函数 Jest 的三个常用 Mock 函数 API 是 jest.fn()jest.spyOn()jest.mock()。 在单元测试中更多时候并不需要关心内部调用方法的执行过程和结果只需要确认是否被正确调用即可。 Mock 函数提供三种特性 捕获函数调用情况设置函数返回值改变函数内部实现 一、Jest.fn() 默认返回 undefined 作为返回值。 const test_click jest.fn(1,2,3);expect(test_click).toHaveBeenCalledTimes(1) // 测试是否调用了 1 次 expect(test_click).toHaveBeenCalledWith(1,2,3) // 测试是否调用了 1 次也可以自定义返回值内部实现 // 自定义返回值 let customFn jest.fn().mockReturnValue(default); expect(customFn()).toBe(default);// 自定义内部实现 let customInside jest.fn((num1, num2) num1 num2); expect(customInside(10, 10)).toBe(20);// 返回 Promise test(jest 返回 Promise, async() {let mockFn jest.fn().mockResolveValue(promise);let result await mockFn();expect(result).toBe(promise);expect(Object.prototype.toString.call(mockFn())).toBe([object Promise]) }) 分组和钩子 使用 describe 关键字对测试进行包裹。 describe(分别测试, () {/** testItem **//** hook 函数 **/ }需要注意的是describe 块的运行顺序总是在 test 函数的执行顺序之前。疑问 // describe 块内程序的执行顺序() describe(1, () {console.log(1);describe(2, () { console.log(2); });describe(3, () {console.log(3);describe(4, () { console.log(4); })describe(5, () { console.log(5); })})describe(6, () { console.log(6); }) }) describe(7, () {console.log(7);it((since there has to be at least one test), () { console.log(8) }); })// 测试结果顺序 1-8可利用 jest 提供的钩子函数对单例进行数据准备工作。 Jest 提供的钩子函数有 beforeEach beforeAll afterEach afterAll 需要注意describe 外部使用的钩子函数和在内部的钩子函数执行顺序 beforeAll(() console.log(outside beforeAll)); beforeEach(() console.log(outside beforeEach)); afterAll(() console.log(outside afterAll)); afterEach(() console.log(outside afterEach));describe(, () {beforeAll(() console.log(intside beforeAll));beforeEach(() console.log(intside beforeEach));afterAll(() console.log(intside afterAll));afterEach(() console.log(intside afterEach));test(test1, () console.log(test1 run));test(test2, () console.log(test2 run)); })/** 结果 outside beforeAll inside beforeAlloutside beforeEach inside beforeEach test1 run inside afterEach outside afterEachoutside beforeEach inside beforeEach test2 run inside afterEach outside afterEachinside afterAll outside afterAll **/测试异步模块 在包括测试的函数里传入 done 参数Jest 会等 done 回调函数执行结束后结束测试。若 done 从未被调用则测试用例执行失败同时输出超时错误。 import timeout from ./timeouttest(测试timer, (done) {timeout(() {expect(22).toBe(4)done()}) })如果异步函数返回 Promise则可以直接将这个 Promise 返回Jest 会等待这个 Promise 的 resolve 状态。如果期待 Promise 被 Reject则需要使用.catch方法并添加expect.assertions来验证一定数量的断言被调用。 test(the data is peanut butter, () {return fetchData().then(data {expect(data).toBe(peanut butter);}); });test(the fetch fails with an error, () {expect.assertions(1);return fetchData().catch(e expect(e).toMatch(error)); });test(the data is peanut butter, () {return expect(fetchData()).resolves.toBe(peanut butter); });test(the fetch fails with an error, () {return expect(fetchData()).rejects.toMatch(error); });借助 async / await test(the data is peanut butter, async () {const data await fetchData();expect(data).toBe(peanut butter); });test(the fetch fails with an error, async () {expect.assertions(1);try {await fetchData();} catch (e) {expect(e).toMatch(error);} });在测试 React 组件时经常能碰到在 useEffect 中更新 State 或异步更新 State 的场景这时测试断言的处理需要借助 Act API 。案例说明和使用方法 配置文件 // 一下列举常用配置项目所有配置属性访问官网手册 https://jestjs.io/docs/configuration// 默认地Jest 会运行所有的测试用例然后产出所有的错误到控制台中直至结束。 // bail 配置选项可以让 Jest 在遇到第一个失败后就停止继续运行测试用例默认值 0 bail: 1, // 每次测试前自动清除模拟调用和实例相当于每次测试前都调用 jest.clearAllMocks默认 false clearMocks: false, // 指示在执行测试时是否应收集覆盖率信息通过使用覆盖率收集语句改造所有已执行文件所以开启后可能会显著减慢 // 测试速度默认 false collectCoverage: false, // 指定测试覆盖率统计范围,使用 glob 模式匹配默认 undefined collectCoverageFrom: [src/**/*.{js,jsx,ts,tsx}, !src/**/*.d.ts], // 指定忽略匹配的覆盖范围,默认 [/node_modules] coveragePathIgnorePatterns: [/node_modules/], // 配置一组在所有测试环境中可用的全局变量, 默认 {} globals: {}, // 用于给模块路径映射到不同的模块, 默认 null moduleNameMapper: {\\.(css|less|scss|sss|styl)$: jest-css-modules | identity-obj-proxy } // 指定 Jest 根目录Jest 只会在根目录下测试用例并运行 rootDir: ./, // 设置 Jest 搜索文件的目录路径列表默认 [] roots: [], // 指定创建测试环境前的准备文件,针对每个测试文件都会运行一次 setupFiles: [react-app-polyfill/jsdom], // 指定测试环境创建完成后为每个测试文件编写的配置文件 setupFilesAfterEnv: [rootDir/src/setupTests.ts], // 配置 Jest 匹配测试文件的规则, 使用 glob 规则 testMatch: [rootDir/src/**/__tests__/**/*.{js,jsx,ts,tsx},rootDir/src/**/*.{spec,test}.{js,jsx,ts,tsx} ], // 用于指定测试用例运行环境默认 node testEnvironment: jsdom, // 配置文件处理模块应该忽略的文件 transformIgnorePatterns: [[/\\\\]node_modules[/\\\\].\\.(js|jsx|ts|tsx)$,^.\\.module\\.(css|sass|scss)$ ], // 指定转换器: js jsx ts tsx 使用 babel-jest 进行转换 css 使用 cssTransform.js 进行转换 其他文件使用 fileTransform.js transform: {^.\\.(js|jsx|ts|tsx)$: rootDir/node_modules/babel-jest,^.\\.css$: rootDir/config/jest/cssTransform.js,^(?!.*\\.(js|jsx|ts|tsx|css|json)$):rootDir/config/jest/fileTransform.js }, // 转化器忽略文件: node_modules 目录下的所有 js jsx ts tsx cssModule 中的所有 css sass scss transformIgnorePatterns: [[/\\\\]node_modules[/\\\\].\\.(js|jsx|ts|tsx)$,^.\\.module\\.(css|sass|scss)$ ], // 测试超时时间默认 5000 毫秒 testTimeout: 5000 实践与调试 当一个测试失败了应该首先检查单独运行该测试用例是否失败。只需要将 test 改为 test.only 便可。 // 编写一条测试 test(describe, () {render(Component /);expect(); })// describe 把多条测试包裹起来进行分组 describe(describe, () {test(test1, fn);test(test2, fn);test.only(test3, fn); // 再次运行测试便只会对该单列进行测试 })当需要在终端查看查找到的 Dom 元素时使用 prettyDOM 对元素进行包括便可浏览贴近 HTML 结构的结果当传递 null 时prettyDOM 返回整个文档的渲染结果。 const div container.querySelector(div); console.log(prettyDOM(div));演示 Demo 仓库React Testing Library 项目准备 npm i create-react-app -g // 全局安装脚手架create-react-app React-Testing-Library // 创建项目yarn add types/react types/react-dom --dev // 基于 TypeScriptyarn add axios // 使用 axios 处理异步请求使用 create-react-app 脚手架创建的项目已经默认使用 Testing Library 作为测试方案但是脚手架默认将工具的一些配置隐藏起来如果希望将配置弹出并进行手动配置则运行 npm run eject 。在弹出工程化配置后只需要关注 jest 和 bable 两个配置。首先在根目录添加jest.config.js 和 bable.config.js 这两个文件然后在 package.json 里把对应位置的配置迁移过来。(jest.config.js 按照配置说明重定义疑问?) // jest.config.js const Config {bail: 1,clearMocks: true,coverageProvider: v8,moduleFileExtensions: [js, jsx, ts, tsx, json],rootDir: ./,setupFilesAfterEnv: [rootDir/src/tests/setupTests.js],testEnvironment: jsdom,testMatch: [rootDir/src/**/__tests__/**/*.[jt]s?(x)],testPathIgnorePatterns: [\\\\node_modules\\\\,],moduleNameMapper: {\\.(css|less|scss|sss|styl)$: identity-obj-proxy}};module.exports Config;创建tests 文件夹添加 setupTests.js 文件然后在 jest.config.js 添加 setupFilesAfterEnv 配置进行覆盖。 // setupTest.js // 导入 extend-expect 目的是 rtl 的一些断言需要建立在这个库上如 toBeInTheDocument import testing-library/jest-dom/extend-expect;// jest.config.js setupFilesAfterEnv: [rootDir/src/tests/setupTests.js]新建 components 文件夹添加 Header 和 List 两个组件。 Header: 在输入框中输入 toDo 内容回车添加到 List 组件中。 List: 一个展示所有 ToDo 项的列表可以进行点击编辑或删除。 具体组件功能结构在此不一一陈列 开展测试 需求分析 Header 组件 input 初始输入框为空input 能输入内容input 回车提交内容input 提交后输入框置空 List 组件 列表为空右上角计数器值为 0 列表不为空右上角计数器存在且值为列表长度列表项删除按钮存在点击可将其删除 列表不为空点击列表项内容可将其修改回车后保存修改后的内容 测试编写 在对应组件下添加 __tests__ 目录创建格式如 *.test.[jt]s?(x) 的测试用例。 // header.test.tsx import React from react; import { render, screen, fireEvent } from testing-library/react;import Header from ../index;let inputNode: HTMLInputElement; const addUndoItem jest.fn(); const inputEvent {target: {value: new todo} }beforeEach(() {render(Header addUndoItem{addUndoItem} /);inputNode screen.getByRole(textbox) as HTMLInputElement; });describe(测试 Header 组件, () {it(初始渲染时输入框为空, () {expect(inputNode).toBeInTheDocument();expect(inputNode.value).toEqual();})it(输入框能输入内容, () {fireEvent.change(inputNode, inputEvent);expect(inputNode.value).toEqual(new todo);})it(输入框回车提交内容并将输入框置空, () {fireEvent.change(inputNode, inputEvent);const enterEvent {keyCode: 13}fireEvent.keyUp(inputNode, enterEvent);expect(addUndoItem).toHaveBeenCalledTimes(1);expect(inputNode.value).toEqual();}) })// list.test.tsx import React from react; import { render, screen, fireEvent, within } from testing-library/react;import List, { IListProps } from ../index;const props: IListProps {list: [{value: list item}],deleteItem: jest.fn(),valueChange: jest.fn(),handleFinish: jest.fn() } let Counter: HTMLElement; let listItemNode: HTMLElement[];beforeEach(() {render(List {...props}/);listItemNode screen.queryAllByRole(listitem) as HTMLElement[]; });describe(测试 List 组件, () {it(初始渲染, () {// 设置空白数据let {deleteItem,valueChange,handleFinish} props;let blank {list: [],deleteItem,valueChange,handleFinish}// 此处重新渲染从中取出 container 容器const { container } render(List {...blank}/);const li container.querySelector(li);// 初始渲染时列表项内容为空计数器值为 0expect(li).toBeNull();Counter screen.getByText(/0/i);expect(Counter).toBeInTheDocument();})it(删除列表项, () {// 列表项不为空时右上角计数器存在且值为列表长度expect(listItemNode).toHaveLength(1);Counter screen.getByText(/1/i);expect(Counter).toBeInTheDocument();// 列表项删除按钮存在点击将其删除let deleteBtn listItemNode[0].querySelector(div) as HTMLElement;expect(deleteBtn).not.toBeNull();fireEvent.click(deleteBtn);expect(props.deleteItem).toHaveBeenCalledTimes(1);})it(编辑列表项, () {// 点击列表项后可将其内容修改fireEvent.click(listItemNode[0]);let editInput within(listItemNode[0]).getByRole(textbox) as HTMLInputElement;const editValue {target: {value: edit todo}}// 检查修改回调调用次数fireEvent.change(editInput, editValue);expect(props.valueChange).toHaveBeenCalledTimes(1);const keyUp {keyCode: 13}// handleFinish 在输入框失去焦点和回车确认时触发事件fireEvent.keyUp(editInput, keyUp);// 注意toHaveBeenCalledTimes 断言是记录历史调用次数// 所有在这里想要判断 valueChange 不被调用参数应该为 1 而不是 0expect(props.valueChange).toHaveBeenCalledTimes(1);// change 直接修改 input 的 value 值不触发失焦事件expect(props.handleFinish).toHaveBeenCalledTimes(1);}) }) 编写测试时需要注意的是如果组件的更新动作是交由外部进行处理的便不能期待进行某些操作后能得到与业务中一样的反馈应该通过测试回调函数的响应的侧面验证其功能的可行性。 其他场景 异步操作 对于测试需要等待的响应事件或 Promise使用 await 或 then 来进行处理。 // 场景一 // 点击按钮后异步更新其 textContent 文本可以借助 findBy 异步查询 API 查找需要等待更新的元素 const button screen.getByRole(button, {name: Click Me}) fireEvent.click(button) await screen.findByText(Clicked once) fireEvent.click(button) await screen.findByText(Clicked twice)// 场景二 // 需要等待回调函数的结果使用 waitFor 对断言进行判断 await waitFor(() expect(mockAPI).toHaveBeenCalledTimes(1))// 场景三 // 需要等待从 DOM 中删除元素 waitForElementToBeRemoved(document.querySelector(div.getOuttaHere)).then(() console.log(Element no longer in DOM), )Rudux 测试 对于特别复杂的 redux可以选择对其 reducer 和 effect 使用基本的单元测试。更多场景下对 redux connect 的组件使用 集成测试 。可以显式传递 mock store 也可以使用 Redux Provider 包裹组件。 另一测试方案是单独测试 redux 和组件单独导入未进行 connect 连接的组件使用 Mock 方法模拟其 dispatch测试响应性。 官网案例 测试 Redux 连接的组件 // 建立带有 redux 测试的自定义 render, 后续测试集成组件便可以使用该自定义 render // test-utils.jsx import React from react import { render as rtlRender } from testing-library/react import { createStore } from redux import { Provider } from react-redux // Import your own reducer import reducer from ../reducerfunction render(ui,{initialState,store createStore(reducer, initialState),...renderOptions} {} ) {function Wrapper({ children }) {return Provider store{store}{children}/Provider}return rtlRender(ui, { wrapper: Wrapper, ...renderOptions }) }// re-export everything export * from testing-library/react // override render method export { render } 问题收集 formatMessage not initialized yet, you should use it after react app mounted #2156. 参考资料一 参考资料二 Could not find required intl object. needs to exist in the component ancestry. Using default message as fallback 参考资料 Testing Library 官网文档 React Testing library 使用总结 Jest 官网文档
http://www.w-s-a.com/news/733384/

相关文章:

  • 手机网站特效做互联网平台要多少钱
  • 做网站广告推广平台旅游网站后台管理系统
  • ppt模板下载免费素材网站php网站开发平台下载
  • 网站推广策划报告航空航天可以做游戏可以视频约会的网站
  • 云南建设学院的网站划分切片来做网站
  • 建设视频网站需要什么知识辽阳建设网站
  • 提供o2o网站建设打扑克网站推广软件
  • 制作简单门户网站步骤中国建设局网站查询
  • 漳州专业网站建设网站建设的面试要求
  • 有哪些网站是封面型汕头网站上排名
  • 自动优化网站软件没有了做的新网站做百度推广怎么弄
  • 高陵县建设局网站商标查询网站
  • 郑州建设网站哪家好东莞网络公司排行榜
  • 成都网站开发费用做行程的网站
  • 做地铁建设的公司网站手机网站首页布局设计
  • 福建亨立建设集团有限公司网站搜狗网页游戏大厅
  • 设计网站musil访问量大的网站选择多少流量的服务器何时
  • 公司网站包括哪些内容新网站怎样做外链
  • 淘宝宝贝链接怎么做相关网站广州好蜘蛛网站建设
  • 长春网站制作网页博山区住房和城乡建设局网站
  • 云南大学网站建设解析到网站怎样做
  • 网站维护的要求包括锦溪网站建设
  • 金站网.营销型网站学校安全教育网站建设
  • 临沂市建设局网站公示军事新闻头条2023
  • 购物网网站建设lamp 做网站
  • 做网站网站庄家html5网站开发技术
  • 无锡门户网站制作电话广告设计公司的未来
  • 白云区专业网站建设网页设计模拟试题答案
  • 毕业设计网站代做多少钱制作旅游网站设计概述
  • 网站开发维护运维无人在线电视剧免费观看