单元测试框架

Java​ 中,​JUnit ​和 ​TestNG ​是最受欢迎的单元测试框架。

  • JUnit
  • TestNG

JUnit

首先是大名鼎鼎的 JUnit ,JUnit 已经成为 Java 应用程序单元测试的事实标准

JUnit 是一个开源的 Java 语言的单元测试框架,专门针对 Java 设计,使用最广泛。JUnit 目前最新版本是 5

JUnit5 的组成:JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

JUnit5 建议使用 Java8 及以上版本

  • JUnit Platform 是在 JVM 上启动测试框架的基础,它定义了TestEngine在平台运行的新测试框架的 API
  • JUnit Jupiter 它用于编写测试代码的新的编程和扩展模型。它具有所有新的 Junit 注释和TestEngine实现来运行这些注释编写的测试。
  • JUnit Vintage JUnit4 已经存在了很长时间,并且有许多以 JUnit4 编写的测试。JUnit Jupiter 还需要支持这些测试。为此,开发了 JUnit Vintage 子项目。提供了一个测试引擎,用于在平台上运行基于 JUnit 3 和 JUnit 4 的测试。它要求 JUnit 4.12 或更高版本出现在类路径或模块路径中。从它的名字 Vintage(古老的;古色古香的)中也能有所体会。

简单例子

我们先来个最简单的例子,别看简单,很多人会犯错

@SpringBootTest
@RunWith(SpringRunner.class)
public class JunitTest {@Testpublic void testJunit(){System.out.println("junit test");}
}
复制代码

很简单对吧,如果你用了 SpringBoot 简单到好像没啥说的,其实不然,我们来聊聊:

首先,这段代码使用的是 JUnit 4 还是 JUnit5 ? 你可能会觉得,4 和 5 没啥区别吧,用哪个不一样吗? 代码能跑不就行了?

不是的,4 和 5 肯定有区别这个不用我说了。能跑没问题,但如果你不管是 4 还是 5 都认为一样,API 混用,甚至乱用,那这时候测试出现的各种报错,导致你很懵逼,而且不知道为什么,一通乱查也不知所然。

上面这段代码其实是 JUnit 4 版本,我们看一下 import 就一目了然了,然而可能你在开发的时候没太注意这里是 4 还是 5

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
复制代码

这里确定了,使用的是 4 的版本,这里有几个要注意的点:

  • @Test的包是org.junit.Test ,不要搞错了,因为有好几个同名包
  • 需要@RunWith(SpringRunner.class)
  • 测试类和测试方法需要public修饰

我们看下完整的例子:

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;@SpringBootTest
@RunWith(SpringRunner.class)
public class JunitTest {@Testpublic void testJunit(){System.out.println("junit test ");}
}复制代码

这里强调下环境 ,springboot2.2.x 之前支持 JUnit 4

上面有一点提到了 需要 public 修饰的问题,这不很正常吗,为什么要强调?

那是因为 JUnit 5 不需要了,我们看一下用 JUnit 5 来实现的同样的例子 (SpringBoot 2.2.x 之后支持 JUnit 5):

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;@SpringBootTest
class JunitTest {@Testvoid testJunit5(){System.out.println("junit5");}
}
复制代码

这么简单吗? 对,就是这么简单,所以我说 4 和 5 不一样。我们来看区别的地方:

  • @Test的包是org.junit.jupiter.api.Test
  • 不需要@RunWith(SpringRunner.class)
  • 测试类和测试方法不需要public修饰

我见过很多同学在写测试用例时出现的所谓诡异问题,都是因为他自己都没搞清楚用的是 4 还是 5 的情况下将 4 和 5 混用导致的。

如果你的测试用例是 4 ,可以迁移到 5 了,有关 JUnit 4 迁移到 JUnit5

我们再来看一下 pom 依赖这里,你是不是经常看到有关 test 的依赖是这样写的:

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope><exclusions><exclusion><groupId>org.junit.vintage</groupId><artifactId>junit-vintage-engine</artifactId></exclusion></exclusions>
</dependency>
复制代码

为啥? 为什么要排除 junit-vintage-engine ?如果你认真阅读了前文,你应该能猜到为什么了。

JUnit Vintage 是为了兼容 3 和 4 的一个 engine,如果我们的测试代码都用 5 实现,不需要兼容 3 和 4 ,那要它干嘛? 当然是干掉呀,哈哈。

但如果你需要兼容,那请不要那么鲁莽。上面的这段 dependency 主要用于 spring-boot-starter-test2.2.x2.3.x 版本中。spring-boot-starter-test 2.4.x 版本中,已经不再包含 junit-vintage-engine 这个依赖项了

常规套路

Annotations 描述
@BeforeEach 在方法上注解,在每个测试方法运行之前执行。
@AfterEach 在方法上注解,在每个测试方法运行之后执行
@BeforeAll 该注解方法会在所有测试方法之前运行,该方法必须是静态的。
@AfterAll 该注解方法会在所有测试方法之后运行,该方法必须是静态的。
@Test 用于将方法标记为测试方法
@DisplayName 用于为测试类或测试方法提供任何自定义显示名称
@Disable 用于禁用或忽略测试类或方法
@Nested 用于创建嵌套测试类
@Tag 用于测试发现或过滤的标签来标记测试方法或类
@TestFactory 标记一种方法是动态测试的测试工场

常规套路不说了,比较简单,一看就明白,说几个有意思的。

重复性测试

 @RepeatedTest(5)void repeatTest(TestInfo testInfo,RepetitionInfo repetitionInfo){System.out.println("repeat:" + testInfo.getDisplayName());System.out.println("这是第 "+ repetitionInfo.getCurrentRepetition()+ "次重复");}
复制代码

不用自己写 for 循环了,人家自己带重复的注解,上面两个变量也是自己带的,方便拿到重复信息。

基于参数测试

@ParameterizedTest
@ValueSource(strings = {"java", "python", "go"})
void containsChar(String candidate) {assertTrue(candidate.contains("o"));
}
复制代码

如果你的参数少,也不用写循环了,直接写注解里,还挺方便的。

超时测试

    @Test@Timeout(value = 500, unit = TimeUnit.MILLISECONDS)void failsIfExecutionTimeExceeds500Milliseconds() {// fails if execution time exceeds 500 milliseconds}
复制代码

可以设置 超时的单位和时长

在 assert 中也可以测超时,可以这样写:

     // timed out after 5 seconds@Testvoid test_timeout_fail() {// assertTimeout(Duration.ofSeconds(5), () -> delaySecond(10)); // this will failassertTimeout(Duration.ofSeconds(5), () -> delaySecond(1)); // pass}void delaySecond(int second) {try {TimeUnit.SECONDS.sleep(second);} catch (InterruptedException e) {e.printStackTrace();}}
复制代码

并行测试

以上测试用例都是用主线程或者单线程跑的,下面我们玩儿个多线程并行 test

首先你要在你的 classpath 下面建一个文件 junit-platform.properties

接着加两行配置

junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.mode.default=concurrent复制代码

行了,再跑你的用例就是多线程并行执行的了,当然如果用例本来就设计成单线程的看不出来,那可以使用 Repeat 试一下,比如上面讲过的这个:

  @RepeatedTest(5)void repeatTest(TestInfo testInfo,RepetitionInfo repetitionInfo){System.out.println("repeat:" + testInfo.getDisplayName());System.out.println("这是第 "+ repetitionInfo.getCurrentRepetition()+ "次重复");}
复制代码

上面这个是对一个方法的重复执行并行,有时候我们是想让一个类中的多个方法并行,能不能做到? 可以,改下配置就好了

junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = concurrent
junit.jupiter.execution.parallel.mode.classes.default = same_thread
复制代码

如果反过来呢? 多个类并行,类中的方法串行 也可以,还是改配置:

junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = same_thread
junit.jupiter.execution.parallel.mode.classes.default = concurrent
复制代码

MockMVC

你测个 service 测个 dao 很简单,把 Bean 注入就可以了,Controller 怎么测? 我们要利用下 MockMVC 了

MockMvc 实现了对 Http 请求的模拟,能够直接使用网络的形式,转换到 Controller 的调用,这样可以使得测试速度快、不依赖网络环境,而且提供了一套验证的工具,这样可以使得请求的验证统一而且很方便。

我们先看一个简单的例子:

import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import static org.hamcrest.Matchers.containsString;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;@SpringBootTest
@AutoConfigureMockMvc
class HelloControllerTest {@Autowiredprivate MockMvc mockMvc;@Autowiredprivate HelloController helloController;@Testpublic void shouldReturnDefaultMessage() throws Exception {this.mockMvc.perform(get("/hello")).andDo(print()).andExpect(status().isOk()).andExpect(content().string(containsString("Hello World")));}}
复制代码

解释下没见过的注解:

  • @AutoConfigureMockMvc:用于自动配置 MockMvc, 配置后 MockMvc 类可以直接注入

此外我们利用 @Autowired 注入了一个 MockMvc 的 Bean 实例。我们通过这个例子来模拟请求 /hello 这个 Controller 资源,并且通过判断返回的 content 内容是否包含 Hello World 字符串来决定这个用例的执行是否成功。

注意 imports 部分,我们导入了 MockMvcRequestBuilders 的一些静态方法。整个方法就一行代码,解释一下:

  • perform : 执行一个请求
  • andDo : 添加一个结果处理器,表示要对结果做点什么事情,比如此处使用 print():输出整个响应结果信息
  • andExpect : 添加执行完成后的断言

我们看下执行结果:

MockHttpServletRequest:HTTP Method = GETRequest URI = /helloParameters = {}Headers = []Body = nullSession Attrs = {}Handler:Type = com.xiaobox.springbootdemo.controller.HelloControllerMethod = com.xiaobox.springbootdemo.controller.HelloController#hello(String)Async:Async started = falseAsync result = nullResolved Exception:Type = nullModelAndView:View name = nullView = nullModel = nullFlashMap:Attributes = nullMockHttpServletResponse:Status = 200Error message = nullHeaders = [Content-Type:"text/plain;charset=UTF-8", Content-Length:"12"]Content type = text/plain;charset=UTF-8Body = Hello World!Forwarded URL = nullRedirected URL = nullCookies = []
复制代码

我们来看下一个例子


@WebMvcTest
class HelloControllerTest {@Autowiredprivate MockMvc mockMvc;@Autowiredprivate HelloController helloController;@Testpublic void shouldReturnDefaultMessage() throws Exception {this.mockMvc.perform(get("/hello")).andDo(print()).andExpect(status().isOk()).andExpect(content().string(containsString("Hello World")));}}
复制代码

你发现我们只是把 class 头上的注解换成了 @WebMvcTest,其实的没变,是的。但却比上一段代码快 3 倍。为什么?

因为之前的写法会把 Spring 完整的应用上下文全启动了,而 @WebMvcTest 是将测试范围缩小到仅启动 web 层,所以会快。当你只想测试 http 到 controller 这层的时候,可以用 @WebMvcTest 注解。

你甚至还可以告诉框架只启动某一个 controller 这样更快,比如:@WebMvcTest(HomeController.class)

上面是 WebMvcTest 的第一个场景, 我们来看第二个场景:也是测 controller ,但 controller 调用的 service 我们也 mock,不走真正 service 代码逻辑。这在有时你的 service 没准备好,或者不方便直接调用时会很有用

@WebMvcTest
class HelloControllerTest {@Autowiredprivate MockMvc mockMvc;@Autowiredprivate HelloController helloController;@Testpublic void greetingShouldReturnMessageFromService() throws Exception {Mockito.when(service.greet()).thenReturn("Hello, Mock");this.mockMvc.perform(get("/greeting")).andDo(print()).andExpect(status().isOk()).andExpect(content().string(containsString("Hello, Mock")));}}
复制代码

上面的代码我们用到了 Mockito, 可能你听过周杰伦一首新歌叫 Mojito ,对,Mockito 的命名就是对 Mojito(一种传统的古巴高球鸡尾酒)的戏称

简单来说 Mockito 是一个 java 做单元测试的 Mock 框架:site.mockito.org/

解释下我们上面这行代码 Mockito.when(service.greet()).thenReturn("Hello, Mock");

意为: 当调用 service 的 greet 方法的时候,返回值为 “Hello Mock”,其实没真调那个方法,就是 Mock 了一下,直接给了个返回值。用英文说就是 :When the x method is called then return y

当然 Mockito 在假造上是很有实力的,它有丰富的 API 供你组合使用,有兴趣可以看一看文档和源码注释。

讲到这儿,一定有同学会问,只测 Controller ,那我就用 Postman 就行 了,甚至 curl 都行,为啥要写用例,我不写用例。

哈哈,我相信很多后端同学都没认认真真把用例写完,尤其是 controller 这层的,不装逼,我也是。那我们有必要讨论一下 到底是用 Postman 还是用 MockMVC ?

首先说说 MockMVC 的好处:

  • 可编程,这就给了你无限的自由空间,想怎么折腾随便你,你是上帝
  • 除了写的时候花点时间外,调试的时候速度快,而且可配置,你要想只测 controller,就只启动 controller 的上下文就行了
  • 顺便把测试用例写了,测试同学省心了,给自动化测试提供了基础
  • 间接提高代码质量

其他的我不说,我就说最后一点。我注意到一个现象,很多开发同学拿测试同学当工具人,自己写的代码自己不怎么测试,直接交给测试让他们提 BUG,然后改,BUG 多也不觉得害臊。开发是爽了,由于代码质量差,整个项目的进度都被拖慢了。你可能会说这是软件质量管理的问题,是规则制定的有问题,如果出 BUG 扣钱就没这事儿了。

我要说的是,在软件开发这个领域,很多事情不是刻板的死规则,即便是制定了这样的规则,也不一定有效。更多的时候是整个团队的文化和风气,领导者有责任将整个研发团队的文化和风气带向正轨。什么是正轨 ? 其实我们都知道! 我们都知道应该写高质量的代码,bug 少的代码,设计合理的代码,不断重构、不断维护的代码,我们都知道要做好自己的事就会提高整个团队的效率,我们都知道应该写注释、写文档,我们都知道.....

我们都知道,但我们也知道项目时间紧,而且专门有人一遍遍强调 deadLine ,有人关心你的开发进度,关心功能实现了没有,关心老板有没有意见,没有人关心你累不累,关心你几点下的班,关心规划合不合理,关心代码质量高不高,关心与软件真正有关系的一切。所以做一个真正的 软件研发团队的 Leader 不容易,遇到好 Leader 是你的福气。

扯多了,我们回头来看 Postman ,Postman 的好处好像也不用我多说了,确实,如果只是简单的做 Controller 连通测试,用 Postman 一点儿问题没有,也比写程序快,但如果你的需求时有正好是 MockMVC 的优点可以覆盖的地方,那么就动动手,写写程序吧。

测试报告

想成一份漂亮的测试报告 ? 后端同学说了,整那花里胡哨的有啥用呢,简单一点儿不好吗?

好,简单点儿当然可以,但 UI 带给我们的价值不就是一图胜千言嘛,让无论是前端、后端、测试同学都能一目了然,减轻大脑处理信息的成本。

来,我们先上成果

怎么样,还挺好看的吧,我们用的是 Allure 来生成了一个 web 页面,这个页面还有一些简单的交互,整体简洁好看、易用。

下面我们说一下 Allure 怎么和 JUnit 集成的

我们仍然使用 SpringBoot 以及 JUnit 5 ,先修改一下 pom.xml 文件,添加依赖

 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope><exclusions><exclusion><groupId>org.junit.vintage</groupId><artifactId>junit-vintage-engine</artifactId></exclusion></exclusions>
</dependency><!--测试报告 allure --><dependency><groupId>io.qameta.allure</groupId><artifactId>allure-junit5</artifactId><version>2.18.1</version><scope>test</scope></dependency>复制代码

然而我们添加在 build 中两个 plugin

    <plugin><artifactId>maven-surefire-plugin</artifactId><version>2.21.0</version><configuration><testFailureIgnore>false</testFailureIgnore><argLine>-javaagent:"${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar"</argLine><systemProperties><property><name>junit.jupiter.extensions.autodetection.enabled</name><value>true</value></property></systemProperties></configuration><dependencies><dependency><groupId>org.junit.platform</groupId><artifactId>junit-platform-surefire-provider</artifactId><version>1.2.0</version></dependency><dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId><version>${aspectj.version}</version></dependency></dependencies></plugin><plugin><groupId>io.qameta.allure</groupId><artifactId>allure-maven</artifactId><version>2.11.2</version><configuration><reportVersion>2.4.1</reportVersion></configuration></plugin>
复制代码

我们用 brew 在本地安装一下 Allure (我是 mac 就用这个装了,如果你是其他环境参考后面说的文档)

brew install allure复制代码

接着我们调整项目中的测试用例,然后执行:

mvn clean test
复制代码

接着找到你项目中 surefire-reports 的目录位置

然后执行类似如下命令:

# 注意路径改成你自己项目的,这里只是示例
allure serve /home/path/to/project/target/surefire-reports/复制代码

显示如下信息会自动跳转到浏览器,打开测试报告页面。

是不是很简单?

有关 JUnit5 就聊到这儿,日常一般的开发是够用了。更多的细节和功能,绍假设 、 断言等,请看官方文档 ,当然备不注它也有错的时候。

TestNG

TestNG is a testing framework inspired from JUnit and NUnit but introducing some new functionalities that make it more powerful and easier to use, such as:

  • Annotations.
  • Run your tests in arbitrarily big thread pools with various policies available (all methods in their own thread, one thread per test class, etc...).
  • Test that your code is multithread safe.
  • Flexible test configuration.
  • Support for data-driven testing (with @DataProvider).
  • Support for parameters.
  • Powerful execution model (no more TestSuite).
  • Supported by a variety of tools and plug-ins (Eclipse, IDEA, Maven, etc...).
  • Embeds BeanShell for further flexibility.
  • Default JDK functions for runtime and logging (no dependencies).
  • Dependent methods for application server testing.

上面是 TestNG 的官方介绍,看起来比 JUnit 功能还强大。有了前面 Junit 作为引子, 你再看 TestNG,就好理解的多,因为概念上都差不多,只是功能和细节的不同而已。在这里我们不会展开讲 TestNG 了,但是会讨论一下选型的问题。

如果在 JUnit 5 没出来之前,比如 JUnit4 和 3 的时代,我会毫不犹豫地选择 TestNG,为什么?功能强大,好用啊。但是现在 JUnit5 来了,而且推广的势头也很猛,重要的是从功能上也不输 TestNG,那么怎么选呢?

个人觉得:

  • 如果是后端开发,一般还是选 JUnit 5 写单元测试方便简单些,SpringBoot 也内置了 JUnit 开箱即用,从生态和社区上讲即使有坑也好解决些
  • 如果是搞自动化测试的同学,更多的可能还是用 TestNG 方便些,之前很多遗留项目都是用的 TestNG,另外它和自动化测试工具 selenium 的搭配也早已深入人心。从设计理念到 API,都更符合测试同学的思维。

聊聊 Java 的单元测试相关推荐

  1. 聊聊Java的单元测试

    单元测试框架 Java​ 中,​JUnit ​和 ​TestNG ​是最受欢迎的单元测试框架. JUnit TestNG JUnit 首先是大名鼎鼎的 JUnit ,JUnit 已经成为 Java 应 ...

  2. 《开源者说》08期:聊聊Java那些事儿

    <开源者说>08期:聊聊Java那些事儿 发表于2015-06-10 18:23| 4388次阅读| 来源CSDN| 28 条评论| 作者陈秋歌 Java开源者说 陈阳宫力许斌 摘要:20 ...

  3. python 单元测试_聊聊 Python 的单元测试框架(一):unittest

    本文首发于 HelloGitHub 公众号,并发表于 Prodesire 博客. 前言 说到 Python 的单元测试框架,想必接触过 Python 的朋友脑袋里第一个想到的就是 unittest. ...

  4. pytest测试框架_聊聊 Python 的单元测试框架(三):最火的 pytest

    本文首发于 HelloGitHub 公众号,并发表于 Prodesire 博客. 一.介绍 本篇文章是<聊聊 Python 的单元测试框架>的第三篇,前两篇分别介绍了标准库 unittes ...

  5. ABAP和Java的单元测试Unit Test

    ABAP ABAP class单元测试的执行入口,CLASS_SETUP, 是硬编码在单元测试框架实现CL_AUNIT_TEST_CLASS里的. 待执行的单元测试方法通过CL_AUNIT_TEST_ ...

  6. gc java root_聊聊Java的GC机制

    原标题:聊聊Java的GC机制 近日,MIUI在小米全球社区发布公告,表示MIUI将在全球市场销售的手机中预装谷歌拨号及谷歌消息应用程序(中国.印度.印度尼西亚等市场除外).小米表示,小米9T Pro ...

  7. java整段标记_聊聊JAVA GC系列(7) - 标记整理算法

    在介绍"平平无奇"的标记清除算法时, 还留下了另一个问题, 就是内存碎片的问题. 内存碎片的问题是指, 每次回收的内存都是比较分散的, 可以加起来是一个比较大的数值, 但是由于可用 ...

  8. python split()方法_聊聊 Python 的单元测试框架(一):unittest

    本文首发于 HelloGitHub 公众号,并发表于 Prodesire 博客. 前言 说到 Python 的单元测试框架,想必接触过 Python 的朋友脑袋里第一个想到的就是 unittest. ...

  9. java 获取泛型_聊聊Java泛型擦除那些事

    >版权申明]非商业目的注明出处可自由转载 博文地址:https://blog.csdn.net/ShuSheng0007/article/details/89789849 出自:shushen ...

最新文章

  1. Android @id和@+id 以及 @android:id 的区别
  2. 正态分布,此际回头看,原来并没有事
  3. 项目中常用的 19 条 MySQL 优化总结
  4. IntelliJ IDEA for Mac在MacOS模式下的选择快捷键(Select Shortcut)
  5. Java == 与 equals 的不同
  6. Sendmail邮件服务器搭建与配置笔记
  7. 第11章 数据库安全性
  8. python隐式调用_python 的隐式指针特征与class inheritance
  9. 10.1 Python图像处理之边缘算子-Sobel算子、Roberts算子、拉普拉斯算子、Canny算子、Prewitt算子、高斯拉普拉斯算子
  10. 第十二课:树莓驱动SG90派舵机
  11. 走进JavaWeb技术世界11:单元测试框架Junit
  12. 洛谷 P3373 【模板】线段树 2
  13. 模拟城市服务器连接中断 正试着,【模拟城市5】确认DRM在线 中断不会被踢
  14. android 黄金颜色,金光闪闪-六款经典款黄金首饰推荐
  15. 回归常态啦 2020.12.08日记
  16. Fedora Firefox flash-player插件安装
  17. kbd通达2017版破解后,自定义菜单无法调用表单开启流程/kbdkbd 错误,请联系管理员 /general/approve_center/new/insert.php/kbd
  18. 【opencv人脸识别1】从图片中检测人脸
  19. 小程序开发:完整发布上线流程
  20. php阿里云oss-sdk上传失败情况处理

热门文章

  1. 【黑金ZYNQ7000系列原创视频教程】06.ZYNQ来自FPGA的中断mdash;mdash;按键中断实验...
  2. 数据中心模块化、标准化、预制化、定制化、智能化……傻傻分不清楚?大咖来帮你!...
  3. 存储主控芯片亚洲新势力崛起
  4. 单文件php目录列表源码,分享个漂亮的php目录列表源码。(很多人问我的)
  5. 简单的python毕设选题推荐
  6. HTML期末大作业~玄幻情缘游戏官网静态html模板
  7. 解决 websockets.exceptions.ConnectionClosedOK: sent xxxx (xx); then received xxxx
  8. php基础(7)_运算符
  9. 百度地图API(二)轨迹回放
  10. 东华大学计算机蔡博士,蔡雅琦博士为信息学院学生做“大学生人际关系”讲座...