selenium架构_Selenium测试的干净架构

selenium架构

在此博客文章中,我想介绍一种具有最佳设计模式的Selenium测试的简洁架构:页面对象,页面元素(通常称为HTML包装器)以及自行开发的非常小巧的框架。 该体系结构不限于示例中使用的Java,也可以以任何其他语言应用于Selenium测试。

定义和关系。

页面对象 。 页面对象封装了网页的行为。 每个网页有一个页面对象,将页面的逻辑抽象到外部。 这意味着,与网页的交互被封装在page对象中。 Selenium的“ 通过”定位器来查找页面上的元素也没有对外公开。 页面对象的调用者不应忙于By定位符,例如By.idBy.tageNameBy.cssSelector。Selenium测试类对页面对象进行操作。 从网上商店举个例子:页面对象类可以称为例如ProductPageShoppingCartPagePaymentPage等。这些始终是带有自己的URL的整个网页的类。

页面元素 (又名HTML包装器 )。 页面元素是网页的另一个细分。 它表示一个HTML元素,并封装了与此元素进行交互的逻辑。 我将页面元素称为HTML包装器。 HTML包装器是可重用的,因为多个页面可以包含相同的元素。 例如,用于DatepickerHTML包装器可以提供以下方法(API):“在输入字段中设置日期”,“打开日历弹出窗口”,“在日历弹出窗口中选择给定的日期”等。其他HTML包装可以例如,自动完成,面包屑,复选框,单选按钮,MultiSelect,消息等。HTML包装器可以是复合的。 这意味着它可以包含多个小元素。 例如,产品目录由产品组成,购物车由项目组成,等等。内部元素的Selenium的“ 按”定位符封装在复合页面元素中。

Martin Fowler描述了页面对象和HTML包装器作为设计模式。

Selenium测试类的骨骼结构。

测试类的结构良好。 它以单个过程步骤的形式定义测试顺序。 我建议采用以下结构:

public class MyTestIT extends AbstractSeleniumTest {@FlowOnPage(step = 1, desc = "Description for this method")void flowSomePage(SomePage somePage) {...}@FlowOnPage(step = 2, desc = "Description for this method")void flowAnotherPage(AnotherPage anotherPage) {...}@FlowOnPage(step = 3, desc = "Description for this method")void flowYetAnotherPage(YetAnotherPage yetAnotherPage) {...}...
}

MyTestIT类是用于集成测试的JUnit测试类。 @FlowOnPage是网页上测试逻辑的方法注释。 step参数定义测试序列中的序列号。 计数从1开始。这意味着,将在步骤= 2的方法之前处理步骤= 1的带注释的方法。第二个参数desc表示描述该方法的作用。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FlowOnPage {int step() default 1;String desc();
}

带页面对象作为方法参数调用带注释的方法。 通常,每次单击按钮或链接都会切换到下一页。 开发的框架应确保在调用带有下一步的带注释的方法之前,已完全加载下一页。 下图说明了测试类,页面对象和HTML包装器之间的关系。

Selenium

别这样 用@Test注释的JUnit方法在哪里,解析@FlowOnPage注释的逻辑在哪里 ? 该代码隐藏在超类AbstractSeleniumTest中

public abstract class AbstractSeleniumTest {// configurable base URLprivate final String baseUrl = System.getProperty("selenium.baseUrl", "http://localhost:8080/contextRoot/");private final WebDriver driver;public AbstractSeleniumTest() {// create desired WebDriverdriver = new ChromeDriver();// you can also set here desired capabilities and so on...}/*** The single entry point to prepare and run test flow.*/@Testpublic void testIt() throws Exception {LoadablePage lastPageInFlow = null;List <Method> methods = new ArrayList<>();// Seach methods annotated with FlowOnPage in this and all super classesClass c = this.getClass();while (c != null) {for (Method method: c.getDeclaredMethods()) {if (method.isAnnotationPresent(FlowOnPage.class)) {FlowOnPage flowOnPage = method.getAnnotation(FlowOnPage.class);// add the method at the right positionmethods.add(flowOnPage.step() - 1, method);}}c = c.getSuperclass();}for (Method m: methods) {Class<?>[] pTypes = m.getParameterTypes();LoadablePage loadablePage = null;if (pTypes != null && pTypes.length > 0) {loadablePage = (LoadablePage) pTypes[0].newInstance();}if (loadablePage == null) {throw new IllegalArgumentException("No Page Object as parameter has been found for the method " +m.getName() + ", in the class " + this.getClass().getName());}// initialize Page Objects Page-Objekte and set parent-child relationshiploadablePage.init(this, m, lastPageInFlow);lastPageInFlow = loadablePage;}if (lastPageInFlow == null) {throw new IllegalStateException("Page Object to start the test was not found");}// start testlastPageInFlow.get();}/*** Executes the test flow logic on a given page.** @throws AssertionError can be thrown by JUnit assertions*/public void executeFlowOnPage(LoadablePage page) {Method m = page.getMethod();if (m != null) {// execute the method annotated with FlowOnPagetry {m.setAccessible(true);m.invoke(this, page);} catch (Exception e) {throw new AssertionError("Method invocation " + m.getName() +", in the class " + page.getClass().getName() + ", failed", e);}}}@Afterpublic void tearDown() {// close browserdriver.quit();}/*** This method is invoked by LoadablePage.*/public String getUrlToGo(String path) {return baseUrl + path;}public WebDriver getDriver() {return driver;}
}

如您所见,只有一种测试方法testIt可以解析批注,创建具有关系的页面对象并启动测试流程。

页面对象的结构。

每个页面对象类均从LoadablePage类继承,而该类又从Selenium的LoadableComponent类继承。 这篇写得很好的文章对L oadableComponent进行了很好的解释: LoadableComponent的简单和高级用法 。 LoadablePage是我们自己的类,实现如下:

import org.openqa.selenium.support.ui.WebDriverWait;
import org.junit.Assert;
import org.openqa.selenium.By;
import org.openqa.selenium.Keys;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.interactions.Actions;
import org.openqa.selenium.support.PageFactory;
import org.openqa.selenium.support.ui.ExpectedCondition;
import org.openqa.selenium.support.ui.LoadableComponent;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Method;
import java.util.List;public abstract class LoadablePage<T extends LoadableComponent<T>> extends LoadableComponent<T> {private final static Logger LOGGER = LoggerFactory.getLogger(LoadablePage.class);private AbstractSeleniumTest seleniumTest;private String pageUrl;private Method method;private LoadablePage parent;/*** Init method (invoked by the framework).** @param seleniumTest instance of type AbstractSeleniumTest* @param method to be invoked method annotated with @FlowOnPage* @param parent parent page of type LoadablePage*/void init(AbstractSeleniumTest seleniumTest, Method method, LoadablePage parent) {this.seleniumTest = seleniumTest;this.pageUrl = seleniumTest.getUrlToGo(getUrlPath());this.method = method;this.parent = parent;PageFactory.initElements(getDriver(), this);}/*** Path of the URL without the context root for this page.** @return String path of the URL*/protected abstract String getUrlPath();/**** Specific check which has to be implemented by every page object.* A rudimentary check on the basis of URL is undertaken by this class.* This method is doing an extra check if the page has been proper loaded.** @throws Error thrown when the check fails*/protected abstract void isPageLoaded() throws Error;@Overrideprotected void isLoaded() throws Error {// min. check against the page URLString url = getDriver().getCurrentUrl();Assert.assertTrue("You are not on the right page.", url.equals(pageUrl));// call specific check which has to be implemented on every pageisPageLoaded();}@Overrideprotected void load() {if (parent != null) {// call the logic in the parent pageparent.get();// parent page has navigated to this page (via click on button or link).// wait until this page has been loaded.WebDriverWait wait = new WebDriverWait(getDriver(), 20, 250);wait.until(new ExpectedCondition<Boolean> () {@Overridepublic Boolean apply(WebDriver d) {try {isLoaded();return true;} catch (AssertionError e) {return false;}}});} else {// Is there no parent page, the page should be navigated directlyLOGGER.info("Browser: {}, GET {}", getDriver(), getPageUrl());getDriver().get(getPageUrl());}}/*** Ensure that this page has been loaded and execute the test code on the this page.** @return T LoadablePage*/public T get() {T loadablePage = super.get();// execute flow logicseleniumTest.executeFlowOnPage(this);return loadablePage;}/*** See {@link WebDriver#findElement(By)}*/public WebElement findElement(By by) {return getDriver().findElement(by);}/*** See {@link WebDriver#findElements(By)}*/public List<WebElement> findElements(By by) {return getDriver().findElements(by);}public WebDriver getDriver() {return seleniumTest.getDriver();}protected String getPageUrl() {return pageUrl;}Method getMethod() {return method;}
}

如您所见,每个页面对象类都需要实现两个抽象方法:

/*** Path of the URL without the context root for this page.** @return String path of the URL*/
protected abstract String getUrlPath();/**** Specific check which has to be implemented by every page object.* A rudimentary check on the basis of URL is undertaken by the super class.* This method is doing an extra check if the page has been proper loaded.** @throws Error thrown when the check fails*/
protected abstract void isPageLoaded() throws Error;

现在,我想显示一个具体页面对象的代码和一个测试SBB Ticket Shop的测试类,以便读者可以对页面对象进行测试。 页面对象TimetablePage包含基本元素HTML包装器。

public class TimetablePage extends LoadablePage<TimetablePage> {@FindBy(id = "...")private Autocomplete from;@FindBy(id = "...")private Autocomplete to;@FindBy(id = "...")private Datepicker date;@FindBy(id = "...")private TimeInput time;@FindBy(id = "...")private Button search;@Overrideprotected String getUrlPath() {return "pages/fahrplan/fahrplan.xhtml";}@Overrideprotected void isPageLoaded() throws Error {try {assertTrue(findElement(By.id("shopForm_searchfields")).isDisplayed());} catch (NoSuchElementException ex) {throw new AssertionError();}}public TimetablePage typeFrom(String text) {from.setValue(text);return this;}public TimetablePage typeTo(String text) {to.setValue(text);return this;}public TimetablePage typeTime(Date date) {time.setValue(date);return this;}public TimetablePage typeDate(Date date) {date.setValue(date);return this;}public TimetablePage search() {search.clickAndWaitUntil().ajaxCompleted().elementVisible(By.cssSelector("..."));return this;}public TimetableTable getTimetableTable() {List<WebElement> element = findElements(By.id("..."));if (element.size() == 1) {return TimetableTable.create(element.get(0));}return null;}
}

在页面对象中,可以通过@FindBy@FindBys@FindAll批注或按需动态创建HTML包装器(简单或复合),例如,如TimetableTable.create(element) ,其中element是基础WebElement 。 通常,注释不适用于自定义元素。 默认情况下,它们仅与Selenium的WebElement一起使用。 但是让他们也使用自定义元素并不难。 您必须实现一个扩展DefaultFieldDecorator的自定义FieldDecorator 。 自定义FieldDecorator允许对自定义HTML包装程序使用@FindBy@FindBys@FindAll批注。 此处提供了一个示例项目,其中提供了实现细节和自定义元素的示例。 您还可以赶上Selenium的臭名昭著的StaleElementReferenceException在您的自定义FieldDecorator并重新创建由最初定位的基础WebElement。 框架用户看不到StaleElementReferenceException,并且即使在此期间更新了引用的DOM元素(从DOM删除并再次添加新内容)时,也可以在WebElement上调用方法。 此处提供了带有代码段的想法。

好吧,让我展示测试课程。 在测试班中,我们要测试当16岁以下的儿童没有父母旅行时购物车中是否出现提示。 首先,我们必须输入“从”和“到”的车站,在时间表中单击所需的连接,然后在下一页添加一个子项,该子项显示所选连接的旅行报价。

public class HintTravelerIT extends AbstractSeleniumTest {@FlowOnPage(step = 1, desc = "Seach a connection from Bern to Zürich and click on the first 'Buy' button")void flowTimetable(TimetablePage timetablePage) {// Type from, to, date and timetimetablePage.typeFrom("Bern").typeTo("Zürich");Date date = DateUtils.addDays(new Date(), 2);timetablePage.typeDate(date);timetablePage.typeTime(date);// search for connectionstimetablePage.search();// click on the first 'Buy' buttonTimetableTable table = timetablePage.getTimetableTable();table.clickFirstBuyButton();}@FlowOnPage(step = 2, desc = "Add a child as traveler and test the hint in the shopping cart")void flowOffers(OffersPage offersPage) {// Add a childDateFormat df = new SimpleDateFormat("dd.MM.yyyy", Locale.GERMAN);String birthDay = df.format(DateUtils.addYears(new Date(), -10));offersPage.addTraveler(0, "Max", "Mustermann", birthDay);offersPage.saveTraveler();// Get hintsList<String> hints = offersPage.getShoppingCart().getHints();assertNotNull(hints);assertTrue(hints.size() == 1);assertEquals("A child can only travel under adult supervision", hints.get(0));}
}

HTML包装器的结构。

我建议为所有HTML包装器创建一个抽象基类。 我们称之为HtmlWrapper 。 此类可以提供一些常用方法,例如clickclickAndWaitUntilfindElement(s)getParentElementgetAttributeisDisplayed ……对于可编辑元素,您可以创建一个继承自HtmlWrapper的类EditableWrapper 。 此类可以为可编辑元素提供一些常用方法,例如: clear (清除输入), enter (按下Enter键), isEnabled (检查元素是否已启用),…。所有可编辑元素都应继承自EditableWrapper 。 此外,您可以分别为单值和多值元素提供两个接口EditableSingleValueEditableMultipleValue 。 下图展示了这个想法。 它显示了三个基本HTML包装的类层次结构:

  • 日期选择器 。 它继承自EditableWrapper并实现EditableSingleValue接口。
  • MultiSelect 。 它继承自EditableWrapper并实现EditableMultiValue接口。
  • 留言 。 它直接扩展HtmlWrapper,因为消息不可编辑。

包装纸

您是否需要更多有关HTML包装程序的实现细节? jQuery Datepicker的详细信息可以在这篇出色的文章中找到 。 MultiSelect是著名的Select2小部件的包装。 我已经通过以下方式在项目中实现了包装器:

public class MultiSelect extends EditableWrapper implements EditableMultiValue<String> {protected MultiSelect(WebElement element) {super(element);}public static MultiSelect create(WebElement element) {assertNotNull(element);return new MultiSelect(element);}@Overridepublic void clear() {JavascriptExecutor js = (JavascriptExecutor) getDriver();js.executeScript("jQuery(arguments[0]).val(null).trigger('change')", element);}public void removeValue(String...value) {if (value == null || value.length == 0) {return;}JavascriptExecutor js = (JavascriptExecutor) getDriver();Object selectedValues = js.executeScript("return jQuery(arguments[0]).val()", element);String[] curValue = convertValues(selectedValues);String[] newValue = ArrayUtils.removeElements(curValue, value);if (newValue == null || newValue.length == 0) {clear();} else {changeValue(newValue);}}public void addValue(String...value) {if (value == null || value.length == 0) {return;}JavascriptExecutor js = (JavascriptExecutor) getDriver();Object selectedValues = js.executeScript("return jQuery(arguments[0]).val()", element);String[] curValue = convertValues(selectedValues);String[] newValue = ArrayUtils.addAll(curValue, value);changeValue(newValue);}@Overridepublic void setValue(String...value) {clear();if (value == null || value.length == 0) {return;}changeValue(value);}@Overridepublic String[] getValue() {JavascriptExecutor js = (JavascriptExecutor) getDriver();Object values = js.executeScript("return jQuery(arguments[0]).val()", element);return convertValues(values);}private void changeValue(String...value) {Gson gson = new Gson();String jsonArray = gson.toJson(value);String jsCode = String.format("jQuery(arguments[0]).val(%s).trigger('change')", jsonArray);JavascriptExecutor js = (JavascriptExecutor) getDriver();js.executeScript(jsCode, element);}@SuppressWarnings("unchecked")private String[] convertValues(Object values) {if (values == null) {return null;}if (values.getClass().isArray()) {return (String[]) values;} else if (values instanceof List) {List<String> list = (List<String> ) values;return list.toArray(new String[list.size()]);} else {throw new WebDriverException("Unsupported value for MultiSelect: " + values.getClass());}}
}

为了完整起见,还有一个Message实现的示例:

public class Message extends HtmlWrapper {public enum Severity {INFO("info"),WARNING("warn"),ERROR("error");Severity(String severity) {this.severity = severity;}private final String severity;public String getSeverity() {return severity;}}protected Message(WebElement element) {super(element);}public static Message create(WebElement element) {assertNotNull(element);return new Message(element);}public boolean isAnyMessageExist(Severity severity) {List<WebElement> messages = findElements(By.cssSelector(".ui-messages .ui-messages-" + severity.getSeverity()));return messages.size() > 0;}public boolean isAnyMessageExist() {for (Severity severity: Severity.values()) {List<WebElement> messages = findElements(By.cssSelector(".ui-messages .ui-messages-" + severity.getSeverity()));if (messages.size() > 0) {return true;}}return false;}public List<String> getMessages(Severity severity) {List<WebElement> messages = findElements(By.cssSelector(".ui-messages .ui-messages-" + severity.getSeverity() + "-summary"));if (messages.isEmpty()) {return null;}List<String> text = new ArrayList<> ();for (WebElement element: messages) {text.add(element.getText());}return text;}
}

消息将消息组件包装在PrimeFaces中 。

结论

完成页面对象和HTML包装程序的编写后,您可以安心并专注于舒适地编写Selenium测试。 随时分享您的想法。

翻译自: https://www.javacodegeeks.com/2016/04/clean-architecture-selenium-tests.html

selenium架构

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C++ 【随想录】(四)【Makefile】

编译流程 预处理 gcc -E test.c -o test.i test.c源码进行预处理&#xff0c;预处理后停止编译,预处理后文件体积会变大&#xff0c;且为文本格式 编译 gcc -S test.i -o test.s 编译预处理的文件 汇编 gcc -c test.s -o test.o .s转成二进制文件 链接 gcc te…

mfc从文件中读取数据_Python 中的 bytes、str 以及 unicode 区别

从Python发展历史谈起Python3和Python2表示字符序列的方式有所不同。Python3字符序列的两种表示为byte和str。前者的实例包含原始的8位值&#xff0c;即原始的字节&#xff1b;后者的实例包括Unicode字符。Python2字符序列的两种表示为str和unicode。与Python3不同的是&#xf…

linux字符雨,linux周记

shell脚本基础格式要求&#xff1a;首行shebaang机制#!/bin/bash#!/usr/bin/python#!/usr/bin/perlshell脚本用途&#xff1a;自动化常用命令执行系统管理和故障排除创建简单的应用程序处理文本或文件bash中变量的种类局部变量&#xff1a;生效范围为当前shell进程&#xff1b;…

【音视频安卓开发 (一)】AndroidStudio项目配置权限、jni库路径、ABI、Cmake

cmake项目配置 # For more information about using CMake with Android Studio, read the # documentation: https://d.android.com/studio/projects/add-native-code.html# Sets the minimum version of CMake required to build the native library.cmake_minimum_require…

string index out of range_Java 12 骚操作, String居然还能这样玩!

坐稳了&#xff0c;准备起飞&#xff01;1、transformtransform&#xff1a;即字符串转换&#xff0c;来看下 transform 的实现源码&#xff1a;public <R> R transform(Function<? super String, ? extends R> f) {return f.apply(this); }传入一个函数式接口 F…

弹性架构_实践中的弹性基础架构

弹性架构几周前&#xff0c;我获得了一个难得的机会&#xff0c;可以在基础设施领域中沾沾自喜。 在JVM内部的深入了解下&#xff0c;我每天的工作经历发生了有趣的变化&#xff0c;我想与您分享动机和成果。 希望它可以启发类似的问题类别。 背景 我将从解释需要解决方案的上…

linux的ctrl alt f6的作用,Linux(Centous6.4)操作系统中,快捷键Alt+Ctrl+F10是什么作用?...

满意答案John_05152017.07.16一些常用快捷键切换到第一个文本终端。在Linux 下你可以有多达六个不同的终端。这个命令的意思是&#xff1a;“同时按住键和键&#xff0c;然后按键&#xff0c;再释放所有的键”。(n1..6)&#xff1a;切换到第n个文本终端。(你也可以使用不是很经…

【TCP丢包重传】

TCP丢包重传机制如果在网络状况最糟糕的情况下就会造成极大的延迟。或者超过2min断开连接。如果这种场景下可以采用UDP。UDP需要解决包的重新排序&#xff0c;丢包等问题。

前端H5怎么切换语言_「自学系列一」HTML5大前端学习路线+视频教程完整版

全新Java、HTML5前端、大数据、Python爬虫、全链UI设计、软件测试、Unity 3D、Go语言等多个技术方向的全套视频。面对这么多的知识点&#xff0c;有的盆友就麻爪了……我是谁&#xff1f;我该从哪里开始看&#xff1f;我该怎么看&#xff1f;我该看多少&#xff1f;这&#xff…

r语言在linux下取数据,菜鸟第一步,跪在数据处:R语言读取数据

1. 温故知坑实践是学习知识的最好途径。之前我讲的内容都非常非常基础&#xff0c;包括&#xff1a;(1)什么是R语言&#xff1f;R语言和Rstudio软件的安装&#xff0c;Rstudio的界面介绍&#xff1b;(2)R语言的基本逻辑&#xff0c;基本数据类型&#xff1b;(3)ggplot基础绘图&…

c语言 桌面程序_C语言编程工具:Dev - C++ 简单安装和使用!新手福利!

工欲善其事&#xff0c;必先利其器。——《论语》# 写在前面有关编译器和开发工具可以查看历史文章。# 下载Dev-C操作系统建议Windows 7或10&#xff0c;直接搜索Dev-C下载&#xff0c;或者sourceforge官网下载链接&#xff1a;https://sourceforge.net/projects/orwelldevcpp/…

stub_AccuREST Stub Runner发布

stub最近发布时间不错&#xff01; 我在Too Much Coding博客上的博客更多是关于发布&#xff0c;然后是关于任何具体主题;&#xff09; 在作为Brixton RC1的一部分发布Spring Cloud Sleuth之后&#xff0c;我们刚刚发布了AccuREST 1.0.4版本。 我们修复了一些错误&#xff0c;…

kali linux wifi监听模式,无线渗透教程1:监听无线网络

第一&#xff1a;配置管理无线网卡1.1这里&#xff0c;我们使用tplink wn722n, kali linux插上即用&#xff0c;无需安装驱动。1.2Vmare虚拟机配置如下&#xff1a;如网卡插入到电脑后&#xff0c;先将虚拟机设置成桥接模式之后对虚拟机进行配置之后呢&#xff0c;我们在终端窗…

matlab的7.3版本是什么_王者荣耀:玩不好元歌的3大原因,无论什么版本,元歌起码T1.5_电竞...

很多高段位元歌并不是以秀为核心的&#xff0c;而是他们就像一个冷静的杀手&#xff0c;在一闪即逝却又最合适的时机里&#xff0c;他能正确的选择进场的方式&#xff0c;以及击杀的方式。说白了&#xff0c;元歌的进场手段并没有多少&#xff0c;什么时候用傀儡433上去锁人&am…

【WebRTC---入门篇】(一)WebRTC整体架构

1.绿色部分是WebRTC核心部分(核心库) 2.紫色部分是JS提供的API(应用层) 整体是应用层调用核心层。 核心层,第一层 C++ API 提供给外面的接口。最主要的是(PeerConnedtion 对等连接)。 核心层,第二层 Session 上下文管理层(音视频)。 核心层,第三层[最重要的…

python 创建空的numpy数组_数据分析-NumPy内置函数创建数组

微信公众号&#xff1a;yale记关注可了解更多的教程问题或建议&#xff0c;请公众号留言。背景介绍今天学习使用numpy的内置函数arange()、ones()、zeros()、linspace() 等内置函数创建数组&#xff0c;对于使用数据结构和多维列表非常有用&#xff0c;可以节省大量的时间。入门…

使用t-sql语句修改表中的某些数据及数据类型。_数据库基本理论详细介绍

1、数据库范式第一范式&#xff1a;列不可分&#xff0c;eg:【联系人】(姓名&#xff0c;性别&#xff0c;电话)&#xff0c;一个联系人有家庭电话和公司电话&#xff0c;那么这种表结构设计就没有达到 1NF&#xff1b;第二范式&#xff1a;有主键&#xff0c;保证完全依赖。eg…

【WebRTC---入门篇】(三)WebRTC运行机制

轨和流 Track 轨&#xff1a;音频轨和视频轨是不相交的 MediaStream 媒体流&#xff1a;中包含多个音视频轨 WebRTC重要类 MediaStream 负责添加N个轨&#xff0c;加入到RTCPeerConnection RTCPeerConnection 核心 RTCDataChannel 非音视频数据&#xff0c;通过RTCP…

a - 数据结构实验之串一:kmp简单应用_【在线教学示范课案例】数据结构(刘航)...

一、教师简介刘航&#xff0c;网络空间安全学院教师&#xff0c;本科生“数据结构”、“算法设计综合实验” 和研究生“算法设计与优化”课程的主讲教师。近年来积极开展了教学与考核模式改革、探究式/研究型课程等课程改革探索&#xff0c;探索课程、学科竞赛和项目相结合的科…