很难为干净的代码找到一个好的定义,因为我们每个人都有自己的单词clean的定义。 但是,有一个似乎是通用的定义:

干净的代码易于阅读。

这可能会让您感到有些惊讶,但是我认为该定义也适用于测试代码。 使测试尽可能具有可读性是我们的最大利益,因为:

  • 如果我们的测试易于阅读,那么很容易理解我们的代码是如何工作的。
  • 如果我们的测试易于阅读,那么如果测试失败(不使用调试器),很容易发现问题。

编写干净的测试并不难,但是需要大量的实践,这就是为什么如此多的开发人员为此苦苦挣扎的原因。

我也为此感到挣扎,这就是为什么我决定与您分享我的发现的原因。

这是本教程的第四部分,描述了我们如何编写干净的测试。 这次我们将学习为什么不使用new关键字在测试方法中创建对象的原因。 我们还将学习如何用工厂方法和测试数据构建器替换new关键字。

新不是新黑

在本教程中,我们一直在重构单元测试,以确保当使用唯一的电子邮件地址和社交登录提供者创建新用户帐户时, RepositoryUserService类的registerNewUserAccount(RegistrationForm userAccountData)方法能够按预期工作。

RegistrationForm类是一个数据传输对象(DTO) ,我们的单元测试使用setter方法设置其属性值。 我们的单元测试的源代码如下所示(相关代码突出显示):

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
import org.springframework.security.crypto.password.PasswordEncoder;import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;@RunWith(MockitoJUnitRunner.class)
public class RepositoryUserServiceTest {private static final String REGISTRATION_EMAIL_ADDRESS = "john.smith@gmail.com";private static final String REGISTRATION_FIRST_NAME = "John";private static final String REGISTRATION_LAST_NAME = "Smith";private static final Role ROLE_REGISTERED_USER = Role.ROLE_USER;private static final SocialMediaService SOCIAL_SIGN_IN_PROVIDER = SocialMediaService.TWITTER;private RepositoryUserService registrationService;@Mockprivate PasswordEncoder passwordEncoder;@Mockprivate UserRepository repository;@Beforepublic void setUp() {registrationService = new RepositoryUserService(passwordEncoder, repository);}@Testpublic void registerNewUserAccount_SocialSignInAndUniqueEmail_ShouldCreateNewUserAccountAndSetSignInProvider() throws DuplicateEmailException       {RegistrationForm registration = new RegistrationForm();registration.setEmail(REGISTRATION_EMAIL_ADDRESS);registration.setFirstName(REGISTRATION_FIRST_NAME);registration.setLastName(REGISTRATION_LAST_NAME);registration.setSignInProvider(SOCIAL_SIGN_IN_PROVIDER);when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(null);when(repository.save(isA(User.class))).thenAnswer(new Answer<User>() {@Overridepublic User answer(InvocationOnMock invocation) throws Throwable {Object[] arguments = invocation.getArguments();return (User) arguments[0];}});User createdUserAccount = registrationService.registerNewUserAccount(registration);assertEquals(REGISTRATION_EMAIL_ADDRESS, createdUserAccount.getEmail());assertEquals(REGISTRATION_FIRST_NAME, createdUserAccount.getFirstName());assertEquals(REGISTRATION_LAST_NAME, createdUserAccount.getLastName());assertEquals(SOCIAL_SIGN_IN_PROVIDER, createdUserAccount.getSignInProvider());assertEquals(ROLE_REGISTERED_USER, createdUserAccount.getRole());assertNull(createdUserAccount.getPassword());verify(repository, times(1)).findByEmail(REGISTRATION_EMAIL_ADDRESS);verify(repository, times(1)).save(createdUserAccount);verifyNoMoreInteractions(repository);verifyZeroInteractions(passwordEncoder);}
}

那么,有什么问题呢? 我们的单元测试中突出显示的部分很短,而且相对容易阅读。 我认为,此代码的最大问题是它是以数据为中心的。 它创建了一个新的RegistrationForm对象并设置了创建对象的属性值,但是没有描述这些属性值的含义。

如果我们使用new关键字在测试方法中创建新对象,则由于以下原因,我们的测试将变得更难以阅读:

  1. 读者必须知道所创建对象的不同状态。 例如,如果我们考虑示例,读者必须知道如果我们创建一个新的RegistrationForm对象并设置emailfirstNamelastNamesignInProvider属性的属性值,则意味着该对象是一个注册,即通过使用社交登录提供商进行。
  2. 如果创建的对象具有许多属性,则创建该对象的代码会乱码我们测试的源代码。 我们应该记住,即使我们在测试中需要这些对象,我们也应该集中精力描述被测试方法/功能的行为。

尽管不能完全消除这些缺点是不现实的,但我们应尽最大努力将其影响降到最低,并使我们的测试尽可能易于阅读。

让我们找出如何使用工厂方法来做到这一点。

使用工厂方法

当我们使用工厂方法创建新对象时,我们应该以这种方式命名工厂方法及其方法参数,以使我们的代码更易于读写。 让我们看一下两种不同的工厂方法,看看它们对单元测试的可读性有什么样的影响。

这些工厂方法通常添加到对象母类中,因为它们通常对多个测试类有用。 但是,由于我想保持简单,因此将它们直接添加到测试类中。

第一个工厂方法的名称是newRegistrationViaSocialSignIn() ,并且没有方法参数。 在将此工厂方法添加到测试类之后,单元测试的源如下所示(相关部分已突出显示):

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
import org.springframework.security.crypto.password.PasswordEncoder;import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;@RunWith(MockitoJUnitRunner.class)
public class RepositoryUserServiceTest {private static final String REGISTRATION_EMAIL_ADDRESS = "john.smith@gmail.com";private static final String REGISTRATION_FIRST_NAME = "John";private static final String REGISTRATION_LAST_NAME = "Smith";private static final Role ROLE_REGISTERED_USER = Role.ROLE_USER;private static final SocialMediaService SOCIAL_SIGN_IN_PROVIDER = SocialMediaService.TWITTER;private RepositoryUserService registrationService;@Mockprivate PasswordEncoder passwordEncoder;@Mockprivate UserRepository repository;@Beforepublic void setUp() {registrationService = new RepositoryUserService(passwordEncoder, repository);}@Testpublic void registerNewUserAccount_SocialSignInAndUniqueEmail_ShouldCreateNewUserAccountAndSetSignInProvider() throws DuplicateEmailException {RegistrationForm registration = newRegistrationViaSocialSignIn();when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(null);when(repository.save(isA(User.class))).thenAnswer(new Answer<User>() {@Overridepublic User answer(InvocationOnMock invocation) throws Throwable {Object[] arguments = invocation.getArguments();return (User) arguments[0];}});User createdUserAccount = registrationService.registerNewUserAccount(registration);assertEquals(REGISTRATION_EMAIL_ADDRESS, createdUserAccount.getEmail());assertEquals(REGISTRATION_FIRST_NAME, createdUserAccount.getFirstName());assertEquals(REGISTRATION_LAST_NAME, createdUserAccount.getLastName());assertEquals(SOCIAL_SIGN_IN_PROVIDER, createdUserAccount.getSignInProvider());assertEquals(ROLE_REGISTERED_USER, createdUserAccount.getRole());assertNull(createdUserAccount.getPassword());verify(repository, times(1)).findByEmail(REGISTRATION_EMAIL_ADDRESS);verify(repository, times(1)).save(createdUserAccount);verifyNoMoreInteractions(repository);verifyZeroInteractions(passwordEncoder);}private RegistrationForm newRegistrationViaSocialSignIn() {RegistrationForm registration = new RegistrationForm();registration.setEmail(REGISTRATION_EMAIL_ADDRESS);registration.setFirstName(REGISTRATION_FIRST_NAME);registration.setLastName(REGISTRATION_LAST_NAME);registration.setSignInProvider(SOCIAL_SIGN_IN_PROVIDER);return registration;}
}

第一种工厂方法具有以下后果:

  • 测试方法的一部分,它创建了新的RegistrationForm对象,比以前干净得多,并且工厂方法的名称描述了创建的RegistrationForm对象的状态。
  • 我们的模拟对象的配置更难以阅读,因为email属性的值在我们的工厂方法中被“隐藏”了。
  • 因为创建的RegistrationForm对象的属性值被“隐藏”在我们的工厂方法中,所以我们的断言更加难以理解。

如果我们使用对象母模式 ,则问题会更大,因为我们必须将相关的常量移至对象母类。

我认为可以说,尽管第一种工厂方法有其好处,但它也有严重的缺点。

让我们看看第二种工厂方法是否可以消除这些缺点。

第二个工厂方法的名称为newRegistrationViaSocialSignIn() ,并且它将电子邮件地址,名字,姓氏和提供程序中的社交符号作为方法参数。 在将此工厂方法添加到测试类之后,单元测试的源如下所示(相关部分已突出显示):

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
import org.springframework.security.crypto.password.PasswordEncoder;import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;@RunWith(MockitoJUnitRunner.class)
public class RepositoryUserServiceTest {private static final String REGISTRATION_EMAIL_ADDRESS = "john.smith@gmail.com";private static final String REGISTRATION_FIRST_NAME = "John";private static final String REGISTRATION_LAST_NAME = "Smith";private static final Role ROLE_REGISTERED_USER = Role.ROLE_USER;private static final SocialMediaService SOCIAL_SIGN_IN_PROVIDER = SocialMediaService.TWITTER;private RepositoryUserService registrationService;@Mockprivate PasswordEncoder passwordEncoder;@Mockprivate UserRepository repository;@Beforepublic void setUp() {registrationService = new RepositoryUserService(passwordEncoder, repository);}@Testpublic void registerNewUserAccount_SocialSignInAndUniqueEmail_ShouldCreateNewUserAccountAndSetSignInProvider() throws DuplicateEmailException {RegistrationForm registration = newRegistrationViaSocialSignIn(REGISTRATION_EMAIL_ADDRESS,REGISTRATION_FIRST_NAME,REGISTRATION_LAST_NAME,SOCIAL_MEDIA_SERVICE);when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(null);when(repository.save(isA(User.class))).thenAnswer(new Answer<User>() {@Overridepublic User answer(InvocationOnMock invocation) throws Throwable {Object[] arguments = invocation.getArguments();return (User) arguments[0];}});User createdUserAccount = registrationService.registerNewUserAccount(registration);assertEquals(REGISTRATION_EMAIL_ADDRESS, createdUserAccount.getEmail());assertEquals(REGISTRATION_FIRST_NAME, createdUserAccount.getFirstName());assertEquals(REGISTRATION_LAST_NAME, createdUserAccount.getLastName());assertEquals(SOCIAL_SIGN_IN_PROVIDER, createdUserAccount.getSignInProvider());assertEquals(ROLE_REGISTERED_USER, createdUserAccount.getRole());assertNull(createdUserAccount.getPassword());verify(repository, times(1)).findByEmail(REGISTRATION_EMAIL_ADDRESS);verify(repository, times(1)).save(createdUserAccount);verifyNoMoreInteractions(repository);verifyZeroInteractions(passwordEncoder);}private RegistrationForm newRegistrationViaSocialSignIn(String emailAddress, String firstName, String lastName, SocialMediaService signInProvider) {RegistrationForm registration = new RegistrationForm();registration.setEmail(emailAddress);registration.setFirstName(firstName);registration.setLastName(lastName);registration.setSignInProvider(signInProvider);return registration;}
}

第二种工厂方法具有以下后果:

  • 我们的测试方法的一部分(创建新的RegistrationForm对象)比使用第一个工厂方法的相同代码稍微有些混乱。 但是,它仍然比原始代码干净,因为factory方法的名称描述了创建对象的状态。
  • 似乎消除了第一个工厂方法的弊端,因为创建的对象的属性值未“隐藏”在工厂方法内部。

看起来很酷,对吧?

真的很容易想到天堂里一切都很好,但是事实并非如此。 尽管我们已经看到工厂方法可以使我们的测试更具可读性,但事实是,只有满足以下条件,它们才是一个不错的选择:

  1. 工厂方法没有太多的方法参数。 当方法参数的数量增加时,我们的测试将变得更加难以编写和读取。 显而易见的问题是:工厂方法可以有多少个方法参数? 不幸的是,很难给出确切的答案,但是我认为,如果工厂方法只有少数方法参数,那么使用工厂方法是一个不错的选择。
  2. 测试数据没有太大的差异。 使用工厂方法的问题是单个工厂方法通常适用于一个用例。 如果我们需要支持N个用例,则需要N种工厂方法。 这是一个问题,因为随着时间的流逝,我们的工厂方法变得ated肿,混乱并且难以维护(尤其是如果使用对象母模式)。

让我们找出测试数据生成器是否可以解决其中一些问题。

使用测试数据构建器

测试数据构建器是使用构建器模式创建新对象的类。 Effective Java中描述的构建器模式有很多好处 ,但是我们的主要动机是提供一个流畅的API以创建测试中使用的对象。

我们可以按照以下步骤创建一个测试数据构建器类,该类创建新的RegistrationForm对象:

  1. 创建一个RegistrationFormBuilder类。
  2. RegistrationForm字段添加到创建的类。 该字段包含对创建对象的引用。
  3. 向创建的类中添加默认构造函数,并通过创建新的RegistrationForm对象来实现它。
  4. 添加用于设置创建的RegistrationForm对象的属性值的方法。 每个方法都通过调用正确的setter方法来设置属性值,并返回对RegistrationFormBuilder对象的引用。 请记住,这些方法的方法名称可以建立或破坏我们的DSL
  5. 向所创建的类中添加一个build()方法,并通过返回所创建的RegistrationForm对象来实现它。

我们的测试数据构建器类的源代码如下所示:

public class RegistrationFormBuilder {private RegistrationForm registration;public RegistrationFormBuilder() {registration = new RegistrationForm();}public RegistrationFormBuilder email(String email) {registration.setEmail(email);return this;}public RegistrationFormBuilder firstName(String firstName) {registration.setFirstName(firstName);return this;}public RegistrationFormBuilder lastName(String lastName) {registration.setLastName(lastName);return this;}public RegistrationFormBuilder isSocialSignInViaSignInProvider(SocialMediaService signInProvider) {registration.setSignInProvider(signInProvider);return this;}public RegistrationForm build() {return registration;}
}

在修改单元测试以使用新的测试数据构建器类之后,其源代码如下所示(相关部分已突出显示):

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
import org.springframework.security.crypto.password.PasswordEncoder;import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;@RunWith(MockitoJUnitRunner.class)
public class RepositoryUserServiceTest {private static final String REGISTRATION_EMAIL_ADDRESS = "john.smith@gmail.com";private static final String REGISTRATION_FIRST_NAME = "John";private static final String REGISTRATION_LAST_NAME = "Smith";private static final Role ROLE_REGISTERED_USER = Role.ROLE_USER;private static final SocialMediaService SOCIAL_SIGN_IN_PROVIDER = SocialMediaService.TWITTER;private RepositoryUserService registrationService;@Mockprivate PasswordEncoder passwordEncoder;@Mockprivate UserRepository repository;@Beforepublic void setUp() {registrationService = new RepositoryUserService(passwordEncoder, repository);}@Testpublic void registerNewUserAccount_SocialSignInAndUniqueEmail_ShouldCreateNewUserAccountAndSetSignInProvider() throws DuplicateEmailException {RegistrationForm registration = new RegistrationFormBuilder().email(REGISTRATION_EMAIL_ADDRESS).firstName(REGISTRATION_FIRST_NAME).lastName(REGISTRATION_LAST_NAME).isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER).build();when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(null);when(repository.save(isA(User.class))).thenAnswer(new Answer<User>() {@Overridepublic User answer(InvocationOnMock invocation) throws Throwable {Object[] arguments = invocation.getArguments();return (User) arguments[0];}});User createdUserAccount = registrationService.registerNewUserAccount(registration);assertEquals(REGISTRATION_EMAIL_ADDRESS, createdUserAccount.getEmail());assertEquals(REGISTRATION_FIRST_NAME, createdUserAccount.getFirstName());assertEquals(REGISTRATION_LAST_NAME, createdUserAccount.getLastName());assertEquals(SOCIAL_SIGN_IN_PROVIDER, createdUserAccount.getSignInProvider());assertEquals(ROLE_REGISTERED_USER, createdUserAccount.getRole());assertNull(createdUserAccount.getPassword());verify(repository, times(1)).findByEmail(REGISTRATION_EMAIL_ADDRESS);verify(repository, times(1)).save(createdUserAccount);verifyNoMoreInteractions(repository);verifyZeroInteractions(passwordEncoder);}
}

如我们所见,测试数据构建器具有以下优点:

  • 创建新的RegistrationForm对象的代码易于阅读和编写。 我非常喜欢流畅的API,并且我认为这段代码既优美又优雅。
  • 构建器模式确保从我们的测试数据中发现的变化不再是问题,因为我们可以简单地向测试数据构建器类添加新方法。
  • 模拟对象和断言的配置易于阅读,因为常量在我们的测试方法中可见,并且DSL强调每个属性值的含义。

那么,我们应该对所有内容使用构建器模式吗?

没有!

仅在有意义时,才应使用测试数据构建器。 换句话说,我们应该在以下情况下使用它们:

  1. 我们设置了许多属性值。
  2. 我们的测试数据有很大的差异。

如果满足以下条件之一,则构建器模式是一个理想的选择。 原因是我们可以通过命名builder类的setter-like方法来创建特定于域的语言 。 即使我们将创建许多不同的对象并设置许多属性值,这也使我们的测试易于读写。

那是建造者木匠的力量。

如果您想了解有关流利API的更多信息,则应阅读以下文章:

  • 流利的界面
  • Java Fluent API设计器速成课程
  • 用Java构建流畅的API(内部DSL)

今天就这些。 让我们继续并总结从这篇博客文章中学到的知识。

摘要

我们了解了为什么使用new关键字在测试方法中创建对象不是一个好主意,并且我们学习了两种不同的方法来创建在测试中使用的对象。

更具体地说,此博客文章教会了我们三件事:

  • 通过使用new关键字在测试方法中创建所需的对象是一个坏主意,因为它会使我们的测试混乱且难以阅读。
  • 如果我们只需要设置少数几个属性值,而我们的测试数据没有太多变化,则应该使用工厂方法来创建所需的对象。
  • 如果必须设置许多属性值和/或我们的测试数据有很多差异,则应该使用测试数据生成器来创建所需的对象。

翻译自: https://www.javacodegeeks.com/2014/05/writing-clean-tests-new-considered-harmful.html

编写干净的测试-被认为有害的新内容相关推荐

  1. 怎样编写测试类测试分支_编写干净的测试-被认为有害的新内容

    怎样编写测试类测试分支 很难为干净的代码找到一个好的定义,因为我们每个人都有自己的单词clean的定义. 但是,有一个似乎是通用的定义: 简洁的代码易于阅读. 这可能会让您感到有些惊讶,但我认为该定义 ...

  2. 怎样编写测试类测试分支_编写干净的测试–天堂中的麻烦

    怎样编写测试类测试分支 如果我们的代码有明显的错误,我们很有动力对其进行改进. 但是,在某些时候,我们认为我们的代码"足够好"并继续前进. 通常,当我们认为改进现有代码的好处小于所 ...

  3. 怎样编写测试类测试分支_编写干净的测试–从配置开始

    怎样编写测试类测试分支 很难为干净的代码找到一个好的定义,因为我们每个人都有自己的单词clean的定义. 但是,有一个似乎是通用的定义: 简洁的代码易于阅读. 这可能会让您感到有些惊讶,但我认为该定义 ...

  4. 断言工具的编写_编写干净的测试–用特定领域的语言替换断言

    断言工具的编写 很难为干净的代码找到一个好的定义,因为我们每个人都有自己的单词clean的定义. 但是,有一个似乎是通用的定义: 简洁的代码易于阅读. 这可能会让您感到有些惊讶,但我认为该定义也适用于 ...

  5. c++返回指针时候注意提防_编写干净的测试–提防魔术

    c++返回指针时候注意提防 很难为干净的代码找到一个好的定义,因为我们每个人都有自己的单词clean的定义. 但是,有一个似乎是通用的定义: 简洁的代码易于阅读. 这可能会让您感到有些惊讶,但我认为该 ...

  6. 分而治之_编写干净的测试–分而治之

    分而治之 好的单元测试应该仅出于一个原因而失败. 这意味着适当的单元测试仅测试一个逻辑概念. 如果我们要编写干净的测试,则必须识别那些逻辑概念,并且每个逻辑概念只编写一个测试用例. 这篇博客文章描述了 ...

  7. 编写干净的测试–天堂中的麻烦

    如果我们的代码有明显的错误,我们很有动力进行改进. 但是,在某些时候,我们认为我们的代码"足够好"并继续前进. 通常,当我们认为改进现有代码的好处小于所需的工作时,就会发生这种情况 ...

  8. 编写干净的测试–从配置开始

    很难为干净的代码找到一个好的定义,因为我们每个人都有自己的单词clean的定义. 但是,有一个似乎是通用的定义: 干净的代码易于阅读. 这可能会让您感到有些惊讶,但是我认为该定义也适用于测试代码. 使 ...

  9. 编写干净的测试–用特定领域的语言替换断言

    很难为干净的代码找到一个好的定义,因为我们每个人都有自己的单词clean的定义. 但是,有一个似乎是通用的定义: 干净的代码易于阅读. 这可能会让您感到有些惊讶,但是我认为该定义也适用于测试代码. 使 ...

最新文章

  1. 想学python有什么用-我们为什么要选择学习python?学习python有什么用?
  2. CA双向认证的时候,如果一开始下载的证书就有问题的,怎么保证以后的交易没有问题?...
  3. 优化ASP.NET应用程序性能研究与探讨
  4. 公式编辑公式总是偏上怎么办?
  5. jupyter notebook使用opencv的例子_Python安装Jupyter Notebook配置使用教程
  6. Ubuntu 设置下载路径
  7. [渝粤题库]西北工业大学复变函数与积分变换
  8. 一个IO的传奇一生 (9) -- Noop和Deadline调度器
  9. vc++6.0工具栏自绘按钮程序
  10. grafana+zabbix 部署分布式监控系统
  11. mysql连接不用数据库名称_C++连接MySQL数据库
  12. 计算机地图概括的原理,第五章地图概括与自动综合
  13. 斗地主系列之牌型判断
  14. html效果浮窗效果,网页浮窗成效
  15. Linux中awk后面的RS, ORS, FS, OFS 含义
  16. Unity中镜像图片显示
  17. SE、ECA、CA、SA、CBAM、ShuffleAttention、SimAM、CrissCrossAttention、SK、NAM、GAM、SOCA注意力模块、程序
  18. Pycharm 远程debug项目配置
  19. MacOS下iterm,Dracula主题配置
  20. 计算机二级小蒋是一位中学老师,计算机二级excel所有公式样稿.docx

热门文章

  1. php 接收文件 错误代码3,php上传文件错误代码3
  2. Spring Boot 入门 IDEA 版本 2小时学会springBoot 代码上传至gitee 或者github 事务没做出来
  3. matlab eval 不显示,matlab中 eval(command); 运算符无效的问题
  4. i18n国际化登录页面
  5. adf开发_如何在ADF中将参数传递给ActionListener
  6. aws使用技巧_AWS:避免那些“神圣的法案”时刻的一些技巧
  7. openapi_MicroProfile OpenAPI上的Swagger UI
  8. 接口方法javadoc注释_继承Javadoc方法注释
  9. drill apache_Apache Drill 1.4性能增强的简要概述
  10. guava集合操作类的使用_使用Guava进行测试集合的实现