单元测试运行原理探究

2022 年 9 月 26 日 阿里技术


前言


单元测试是软件开发过程中的重要一环,好的单测可以帮助我们更早的发现问题,为系统的稳定运行提供保障。单测还是很好的说明文档,我们往往看单测用例就能够了解到作者对类的设计意图。代码重构时也离不开单测,丰富的单测用例会使我们重构代码时信心满满。虽然单测如此重要,但是一直来都不是很清楚其运行原理,也不知道为什么要做这样或那样的配置,这样终究是不行的,于是准备花时间探究下单测原理,并在此记录。

当在IDEA中Run单元测试时发生了什么?


首先,来看一下当我们直接通过IDEA运行单例时,IDEA帮忙做了哪些事情:
  1. 将工程源码和测试源码进行编译,输出到了target目录

  2. 通过java命令运行com.intellij.rt.junit.JUnitStarter,参数中指定了junit的版本以及单测用例名称

  
  
    
java com.intellij.rt.junit.JUnitStarter -ideVersion5 -junit4 fung.MyTest,test

这里着重追下JUnitStarter的代码,该类在IDEA提供的junit-rt.jar插件包中,具体目录:/Applications/IntelliJ IDEA.app/Contents/plugins/junit/lib/junit-rt.jar。可以将这个包引入到我们自己的工程项目中,方便阅读源码:
JUnitStarter的main函数
public static void main(String[] args) {    List<String> argList = new ArrayList(Arrays.asList(args));    ArrayList<String> listeners = new ArrayList();    String[] name = new String[1];    String agentName = processParameters(argList, listeners, name);    if (!"com.intellij.junit5.JUnit5IdeaTestRunner".equals(agentName) && !canWorkWithJUnitVersion(System.err, agentName)) {        System.exit(-3);    }        if (!checkVersion(args, System.err)) {        System.exit(-3);    }
String[] array = (String[])argList.toArray(new String[0]); int exitCode = prepareStreamsAndStart(array, agentName, listeners, name[0]); System.exit(exitCode);}
这里主要有两个核心方法
...// 处理参数,主要用来确定使用哪个版本的junit框架,同时根据入参填充listenersString agentName = processParameters(argList, listeners, name);...// 启动测试int exitCode = prepareStreamsAndStart(array, agentName, listeners, name[0]);...
接下来看下prepareStreamsAndStart方法运行的时序图,这里以JUnit4为例:
当IDEA确认好要启动的框架版本后,会通过类的全限定名称反射创建IdeaTestRunner<?>的实例。这里以JUnit4为例,IDEA会实例化com.intellij.junit4.JUnit4IdeaTestRunner类对象并调用其startRunnerWithArgs方法,在该方法中会通过buildRequest方法构建org.junit.runner.Request,通过getDescription方法获取org.junit.runner.Description,最后创建org.junit.runner.JUnitCore实例并调用其run方法。
简而言之就是,IDEA最终会借助Junit4框架的能力启动并运行单测用例,所以接下来有必要对Junit4框架的源码做些深入的探究。

Junit4源码探究
Junit是一个由Java语言编写的单元测试框架,已在业界被广泛运用,其作者是大名鼎鼎的Kent Beck和Erich Gamma,前者是《重构:改善既有代码的设计》和《测试驱动开发》的作者,后者则是《设计模式》的作者,Eclipse之父。Junit4发布于2006年,虽然是老古董了,但其中所蕴含的设计理念和思想却并不过时,有必要认真探究一番。
首先我们还是从一个简单的单测用例开始:
public class MyTest {
public static void main(String[] args) { JUnitCore runner = new JUnitCore(); Request request = Request.aClass(MyTest.class); Result result = runner.run(request.getRunner()); System.out.println(JSON.toJSONString(result)); }
@Test public void test1() { System.out.println("test1"); }
@Test public void test2() { System.out.println("test2"); }
@Test public void test3() { System.out.println("test3"); }
}
这里我们不再通过IDEA的插件启动单元测试,而是直接通过main函数,核心代码如下:
public static void main(String[] args) {  // 1. 创建JUnitCore的实例  JUnitCore runner = new JUnitCore();  // 2. 通过单测类的Class对象构建Request  Request request = Request.aClass(MyTest.class);  // 3. 运行单元测试  Result result = runner.run(request.getRunner());  // 4. 打印结果  System.out.println(JSON.toJSONString(result));}
着重看下runner.run(request.getRunner()),先看run函数的代码:
可以看到最终运行哪种类型的测试流程取决于传入的runner实例,即不同的Runner决定了不同的运行流程,通过实现类的名字可以大概猜一猜,JUnit4ClassRunner应该是JUnit4基本的测试流程,MockitoJUnitRunner应该是引入了Mockito的能力,SpringJUnit4ClassRunner应该和Spring有些联系,可能会启动Spring容器。
现在,我们回过头来看看runner.run(request.getRunner())中request.getRunner()的代码:
public Runner getRunner() {  if (runner == null) {    synchronized (runnerLock) {      if (runner == null) {        runner = new AllDefaultPossibilitiesBuilder(canUseSuiteMethod).safeRunnerForClass(fTestClass);      }    }  }  return runner;}  
public Runner safeRunnerForClass(Class<?> testClass) {  try {    return runnerForClass(testClass);  } catch (Throwable e) {    return new ErrorReportingRunner(testClass, e);  }}
public Runner runnerForClass(Class<?> testClass) throws Throwable {  List<RunnerBuilder> builders = Arrays.asList(    ignoredBuilder(),    annotatedBuilder(),    suiteMethodBuilder(),    junit3Builder(),    junit4Builder()  );
for (RunnerBuilder each : builders) { Runner runner = each.safeRunnerForClass(testClass); if (runner != null) { return runner; } } return null;}
可以看到Runner是基于传入的测试类(testClass)的信息选择的,这里的规则如下:
  1. 如果解析失败了,则返回ErrorReportingRunner

  2. 如果测试类上有@Ignore注解,则返回IgnoredClassRunner

  3. 如果测试类上有@RunWith注解,则使用@RunWith的值实例化一个Runner返回

  4. 如果canUseSuiteMethod=true,则返回SuiteMethod,其继承自JUnit38ClassRunner,是比较早期的JUnit版本了

  5. 如果JUnit版本在4之前,则返回JUnit38ClassRunner

  6. 如果上面都不满足,则返回BlockJUnit4ClassRunner,其表示的是一个标准的JUnit4测试模型

我们先前举的那个简单的例子返回的就是BlockJUnit4ClassRunner,那么就以BlockJUnit4ClassRunner为例,看下它的run方法是怎么执行的吧。
首先会先走到其父类ParentRunner中的run方法
@Overridepublic void run(final RunNotifier notifier) {  EachTestNotifier testNotifier = new EachTestNotifier(notifier,                                                       getDescription());  try {    Statement statement = classBlock(notifier);    statement.evaluate();  } catch (AssumptionViolatedException e) {    testNotifier.addFailedAssumption(e);  } catch (StoppedByUserException e) {    throw e;  } catch (Throwable e) {    testNotifier.addFailure(e);  }}
这里有必要展开说下Statement,官方的解释是:Represents one or more actions to be taken at runtime in the course of running a JUnit test suite. 
Statement可以简单理解为对可执行方法的封装和抽象,如RunBefores就是一个Statement,它封装了所有标记了@BeforeClass注解的方法,在运行单例类的用例之前会执行这些方法,运行完后RunBefores还会通过next.evaluate()运行后续的Statement。这里列举一下常见的Statement:
  • RunBefores,会先运行befores里封装的方法(一般是标记了@BeforeClass或@Before),再运行next.evaluate()

  • RunAfters,会先运行next.evaluate(),再运行afters里封装的方法(一般是标记了@AfterClass或@After)

  • InvokeMethod,直接运行testMethod中封装的方法

由此可见,整个单测的运行过程,实际上就是一系列Statement的运行过程,以之前的MyTest为例,它的Statement的执行过程大致可以概况如下:
还剩一个最后问题,实际被测试方法是如何被运行的呢?答案是反射调用。核心代码如下:
@Overridepublic void evaluate() throws Throwable {  testMethod.invokeExplosively(target);}
public Object invokeExplosively(final Object target, final Object... params)  throws Throwable {  return new ReflectiveCallable() {    @Override    protected Object runReflectiveCall() throws Throwable {      return method.invoke(target, params);    }  }.run();}
至此一个标准Junit4的单测用例的执行过程就分析完了,那么像Spring这种需要起容器的单测又是如何运行的呢?接下来就来探究一下。

Spring单测的探究


我们还是以一个简单的例子开始吧
@RunWith(SpringRunner.class)@ContextConfiguration(locations = { "/spring/spring-mybeans.xml" })public class SpringRunnerTest {
@Autowired private MyTestBean myTestBean;
@Test public void test() { myTestBean.test(); }
}
这里先粗略的概括下运行单测时发生了什么。首先,@RunWith注解了该测试类,所以Junit框架会先用SpringRunnerTest.class作为参数创建SpringRunner的实例,然后调用SpringRunner的run方法运行测试,该方法中会启动Spring容器,加载@ContextConfiguration注解指定的Bean配置文件,同时也会处理@Autowired注解为SpringRunnerTest的实例注入myTestBean,最后运行test()测试用例。
简言之就是先通过SpringRunner启动Spring容器,然后运行测试方法。接下来探究一下SpringRunner启动Spring容器的过程。
public final class SpringRunner extends SpringJUnit4ClassRunner {
public SpringRunner(Class<?> clazz) throws InitializationError { super(clazz); }
}
public class SpringJUnit4ClassRunner extends BlockJUnit4ClassRunner {  ...}
SpringRunner和SpringJUnit4ClassRunner实际是等价的,可以认为SpringRunner是SpringJUnit4ClassRunner的一个别名,这里着重看下SpringJUnit4ClassRunner类的实现。
SpringJUnit4ClassRunner继承了BlockJUnit4ClassRunner,前面着重分析过BlockJUnit4ClassRunner,它运行的是一个标准的JUnit4测试模型,SpringJUnit4ClassRunner则是在此基础上做了一些扩展,扩展的内容主要包括:
  1. 扩展了构造函数,多创建了一个TestContextManager实例。

  2. 扩展了createTest()方法,会额外调用TestContextManager的prepareTestInstance方法。

  3. 扩展了beforeClass,在执行@BeforeClass注解的方法前,会先调用TestContextManager的beforeTestClass方法。

  4. 扩展了before,在执行@Before注解的方法前,会先调用TestContextManager的beforeTestMethod方法。

  5. 扩展了afterClass,在执行@AfterClass注解的方法之后,会再调用TestContextManager的afterTestClass方法。

  6. 扩展了after,在执行@After注解的方法之后,会再调用TestContextManager的after方法。

TestContextManager是Spring测试框架的核心类,官方的解释是:TestContextManager is the main entry point into the Spring TestContext Framework. Specifically, a TestContextManager is responsible for managing a single TestContext. 
TestContextManager管理着TestContext,而TestContext则是对ApplicationContext的一个再封装,可以把TestContext理解为增加了测试相关功能的Spring容器。 TestContextManager同时也管理着TestExecutionListeners,这里使用观察者模式提供了对测试运行过程中的关键节点(如beforeClass, afterClass等)的监听能力。
所以通过研究TestContextManager,TestContext和TestExecutionListeners的相关实现类的代码,就不难发现测试时Spring容器的启动秘密了。关键代码如下:
public class DefaultTestContext implements TestContext {  ...  public ApplicationContext getApplicationContext() {    ApplicationContext context = this.cacheAwareContextLoaderDelegate.loadContext(this.mergedContextConfiguration);    if (context instanceof ConfigurableApplicationContext) {      @SuppressWarnings("resource")      ConfigurableApplicationContext cac = (ConfigurableApplicationContext) context;      Assert.state(cac.isActive(), () ->                   "The ApplicationContext loaded for [" + this.mergedContextConfiguration +                   "] is not active. This may be due to one of the following reasons: " +                   "1) the context was closed programmatically by user code; " +                   "2) the context was closed during parallel test execution either " +                   "according to @DirtiesContext semantics or due to automatic eviction " +                   "from the ContextCache due to a maximum cache size policy.");    }    return context;  }  ...}
在DefaultTestContext的getApplicationContext方法中,调用了cacheAwareContextLoaderDelegate的loadContext,最终辗转调到Context的refresh方法,从而构筑起Spring容器上下文。时序图如下:
那么getApplicationContext方法又是在哪里被调用的呢?
前面介绍过,TestContextManager扩展了createTest()方法,会额外调用其prepareTestInstance方法。
public void prepareTestInstance(Object testInstance) throws Exception {  if (logger.isTraceEnabled()) {    logger.trace("prepareTestInstance(): instance [" + testInstance + "]");  }  getTestContext().updateState(testInstance, null, null);
for (TestExecutionListener testExecutionListener : getTestExecutionListeners()) { try { testExecutionListener.prepareTestInstance(getTestContext()); } catch (Throwable ex) { if (logger.isErrorEnabled()) { logger.error("Caught exception while allowing TestExecutionListener [" + testExecutionListener + "] to prepare test instance [" + testInstance + "]", ex); } ReflectionUtils.rethrowException(ex); } }}
prepareTestInstance方法中会调用所有TestExecutionListener的prepareTestInstance方法,其中有一个叫做DependencyInjectionTestExecutionListener的监听器会调到TestContext的getApplicationContext方法。
public void prepareTestInstance(TestContext testContext) throws Exception {  if (logger.isDebugEnabled()) {    logger.debug("Performing dependency injection for test context [" + testContext + "].");  }  injectDependencies(testContext);}
protected void injectDependencies(TestContext testContext) throws Exception {   Object bean = testContext.getTestInstance();   Class<?> clazz = testContext.getTestClass();      // 这里调用TestContext的getApplicationContext方法,构建Spring容器   AutowireCapableBeanFactory beanFactory = testContext.getApplicationContext().getAutowireCapableBeanFactory();      beanFactory.autowireBeanProperties(bean, AutowireCapableBeanFactory.AUTOWIRE_NO, false);   beanFactory.initializeBean(bean, clazz.getName() + AutowireCapableBeanFactory.ORIGINAL_INSTANCE_SUFFIX);   testContext.removeAttribute(REINJECT_DEPENDENCIES_ATTRIBUTE);}
还剩最后一个问题,DependencyInjectionTestExecutionListener是如何被添加的呢?答案是spring.factories
至此Spring单测的启动过程就探究明白了,接下来看下SpringBoot的。

SpringBoot单测的探究


一个简单的SpringBoot单测例子
@RunWith(SpringRunner.class)@SpringBootTest(classes = Application.class)public class MySpringBootTest {
@Autowired private MyTestBean myTestBean;
@Test public void test() { myTestBean.test(); }
}
@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@BootstrapWith(SpringBootTestContextBootstrapper.class)public @interface SpringBootTest {  ...}
粗滤说明一下,这里还是通过SpringRunner的run方法启动测试,其中会启动Spring容器,而@SpringBootTest则提供了启动类,同时通过@BootstrapWith提供的SpringBootTestContextBootstrapper类丰富了TestContext的能力,使得其支持了SpringBoot的一些特性。这里着重探究下@BootstrapWith注解以及SpringBootTestContextBootstrapper。
前面在介绍TestContextManager时,并没有讲到其构造函数以及TestContext的实例化过程,这里将其补上
public TestContextManager(Class<?> testClass) {  this(BootstrapUtils.resolveTestContextBootstrapper(BootstrapUtils.createBootstrapContext(testClass)));}
public TestContextManager(TestContextBootstrapper testContextBootstrapper) {  this.testContext = testContextBootstrapper.buildTestContext();  registerTestExecutionListeners(testContextBootstrapper.getTestExecutionListeners());}
public abstract class AbstractTestContextBootstrapper implements TestContextBootstrapper {    ...  public TestContext buildTestContext() {    return new DefaultTestContext(getBootstrapContext().getTestClass(), buildMergedContextConfiguration(),        getCacheAwareContextLoaderDelegate());  }  ...}
构建DefaultTestContext需要传3个参数:
  • testClass,被测试的类元数据

  • MergedContextConfiguration,封装了声明在测试类上的与测试容器相关的注解,如@ContextConfiguration, @ActiveProfiles, @TestPropertySource

  • CacheAwareContextLoaderDelegate,用来loading或closing容器

那么当我们需要扩展TestContext的功能,或者不想用DefaultTestContext时,应该怎么办呢?最简单的方式自然是新写一个类实现TestContextBootstrapper接口,并覆写buildTestContext()方法,那么如何告诉测试框架要使用新的实现类呢?@BootstrapWith就派上用场了。
这里来看下BootstrapUtils.resolveTestContextBootstrapper的代码
static TestContextBootstrapper resolveTestContextBootstrapper(BootstrapContext bootstrapContext) {  Class<?> testClass = bootstrapContext.getTestClass();
Class<?> clazz = null; try { clazz = resolveExplicitTestContextBootstrapper(testClass); if (clazz == null) { clazz = resolveDefaultTestContextBootstrapper(testClass); } if (logger.isDebugEnabled()) { logger.debug(String.format("Instantiating TestContextBootstrapper for test class [%s] from class [%s]", testClass.getName(), clazz.getName())); } TestContextBootstrapper testContextBootstrapper = BeanUtils.instantiateClass(clazz, TestContextBootstrapper.class); testContextBootstrapper.setBootstrapContext(bootstrapContext); return testContextBootstrapper; } ...}
  
  
    
private static Class<?> resolveExplicitTestContextBootstrapper(Class<?> testClass) {  Set<BootstrapWith> annotations = AnnotatedElementUtils.findAllMergedAnnotations(testClass, BootstrapWith.class);  if (annotations.isEmpty()) {    return null;  }  if (annotations.size() == 1) {    return annotations.iterator().next().value();  }
// 获取@BootstrapWith注解的值 BootstrapWith bootstrapWith = testClass.getDeclaredAnnotation(BootstrapWith.class); if (bootstrapWith != null) { return bootstrapWith.value(); }
throw new IllegalStateException(String.format( "Configuration error: found multiple declarations of @BootstrapWith for test class [%s]: %s", testClass.getName(), annotations));}
这里会通过@BootstrapWith注解的值,实例化定制的TestContextBootstrapper,从而提供定制的TestContext
SpringBootTestContextBootstrapper就是TestContextBootstrapper的实现类,它通过间接继承AbstractTestContextBootstrapper类扩展了创建TestContext的能力,这些扩展主要包括:
  1. 将ContextLoader替换为了SpringBootContextLoader

  2. 增加了DefaultTestExecutionListenersPostProcessor对TestExecutionListener进行增强处理

  3. 增加了对webApplicationType的处理

接下来看下SpringBootContextLoader的相关代码
public class SpringBootContextLoader extends AbstractContextLoader {
@Override public ApplicationContext loadContext(MergedContextConfiguration config) throws Exception { Class<?>[] configClasses = config.getClasses(); String[] configLocations = config.getLocations(); Assert.state( !ObjectUtils.isEmpty(configClasses) || !ObjectUtils.isEmpty(configLocations), () -> "No configuration classes " + "or locations found in @SpringApplicationConfiguration. " + "For default configuration detection to work you need " + "Spring 4.0.3 or better (found " + SpringVersion.getVersion() + ")."); SpringApplication application = getSpringApplication(); // 设置mainApplicationClass application.setMainApplicationClass(config.getTestClass()); // 设置primarySources application.addPrimarySources(Arrays.asList(configClasses)); // 添加configLocations application.getSources().addAll(Arrays.asList(configLocations)); // 获取environment ConfigurableEnvironment environment = getEnvironment(); if (!ObjectUtils.isEmpty(config.getActiveProfiles())) { setActiveProfiles(environment, config.getActiveProfiles()); } ResourceLoader resourceLoader = (application.getResourceLoader() != null) ? application.getResourceLoader() : new DefaultResourceLoader(getClass().getClassLoader()); TestPropertySourceUtils.addPropertiesFilesToEnvironment(environment, resourceLoader, config.getPropertySourceLocations()); TestPropertySourceUtils.addInlinedPropertiesToEnvironment(environment, getInlinedProperties(config)); application.setEnvironment(environment); // 获取并设置initializers List<ApplicationContextInitializer<?>> initializers = getInitializers(config, application); if (config instanceof WebMergedContextConfiguration) { application.setWebApplicationType(WebApplicationType.SERVLET); if (!isEmbeddedWebEnvironment(config)) { new WebConfigurer().configure(config, application, initializers); } } else if (config instanceof ReactiveWebMergedContextConfiguration) { application.setWebApplicationType(WebApplicationType.REACTIVE); if (!isEmbeddedWebEnvironment(config)) { new ReactiveWebConfigurer().configure(application); } } else { application.setWebApplicationType(WebApplicationType.NONE); } application.setInitializers(initializers); // 运行SpringBoot应用 return application.run(); }
}
可以看到这里构建了SpringApplication,设置了mainApplicationClass,设置了primarySources,设置了initializers,最终通过application.run()启动了SpringBoot应用。
至此SpringBoot单测的启动过程也探究明白了,接下来看下Maven插件是如何运行单测的。

Maven插件如何运行单测


我们知道maven是通过一系列的插件帮助我们完成项目开发过程中的构建、测试、打包、部署等动作的,当在Console中运行maven clean test命令时,maven会依次运行以下goal:
  • maven-clean-plugin:2.5:clean,用于清理target目录

  • maven-resources-plugin:2.6:resources,将主工程目录下的资源文件移动到target目录下的classes目录中

  • maven-compiler-plugin:3.1:compile,将主工程目录下的java源码编译为字节码,并移动到target目录下的classes目录中

  • maven-resources-plugin:2.6:testResources,将测试工程目录下的资源文件移动到target目录下的test-classes目录中

  • maven-compiler-plugin:3.1:testCompile,将测试工程目录下的java源码编译为字节码,并移动到target目录下的classes目录中

  • maven-surefire-plugin:2.12.4:test,运行单测

我们扒下maven-surefire-plugin插件的代码看一下。首先引入下maven-surefire-plugin和surefire-junit4包,方便我们查看代码:
<dependency>  <groupId>org.apache.maven.plugins</groupId>  <artifactId>maven-surefire-plugin</artifactId>  <version>2.9</version></dependency>
<dependency> <groupId>org.apache.maven.surefire</groupId> <artifactId>surefire-junit4</artifactId> <version>3.0.0-M7</version></dependency>
核心代码在org.apache.maven.plugin.surefire.AbstractSurefireMojo#execute中,这里就不贴代码了,有兴趣的可以自己看下。总之这里会用JUnit4ProviderInfo中的信息通过反射实例化JUnit4Provider对象,然后调用其invoke方法,在改方法中会最终实例化Runner并调用其run方法。核心代码如下:
private static void execute( Class<?> testClass, Notifier notifier, Filter filter ){  final int classModifiers = testClass.getModifiers();  if ( !isAbstract( classModifiers ) && !isInterface( classModifiers ) )  {    Request request = aClass( testClass );    if ( filter != null )    {      request = request.filterWith( filter );    }    Runner runner = request.getRunner();    if ( countTestsInRunner( runner.getDescription() ) != 0 )    {      runner.run( notifier );    }  }}



总结


至此单元测试运行的相关原理就探究完了,我们来回顾下有哪些内容吧
  1. 通过IDEA直接运行单测时,会通过JUnitStarter的main方法作为入口,最终调用Junit运行单元测试。

  2. Junit4将@Before、@Test、@After这些注解打标的方法都抽象成了Statement,整个单测的运行过程,实际上就是一系列Statement的运行过程。方法的调用是通过反射的方式实现的。

  3. 借助于@RunWith(SpringRunner.class)注解,测试框架会运行SpringRunner实例的run方法,通过TestContextManager创建TestContext,并启动Spring容器。SpringRunner和SpringJUnit4ClassRunner实际上是等价的。

  4. 借助于@SpringBootTest和@BootstrapWith(SpringBootTestContextBootstrapper.class)注解,测试框架通过SpringBootTestContextBootstrapper增强了TestContext,达到了启动SpringBoot应用的目的。

  5. Maven通过运行maven-surefire-plugin:2.12.4:test启动单元测试,其核心是通过JUnit4Provider调用了JUnit框架的代码。

推荐阅读

1.如何写出一篇好的技术方案?

2.阿里10年沉淀|那些技术实战中的架构设计方法

3.如何做好“防御性编码”?

ModelScope开源模型社区评测专场重磅来袭


发布你对ModelScope开源模型社区的体验评测,免费使用模型库搭建属于你的小应用,有机会获得AirPods和阿里云定制礼品等。


点击阅读原文查看详情。

登录查看更多
0

相关内容

代码注释最详细的Transformer
专知会员服务
110+阅读 · 2022年6月30日
【经典书】概率图模型:原理与技术,1270页pdf
专知会员服务
131+阅读 · 2022年2月13日
【2021新书】面向对象的Python编程,418页pdf
专知会员服务
70+阅读 · 2021年12月15日
【干货书】面向计算科学和工程的Python导论,167页pdf
专知会员服务
41+阅读 · 2021年4月7日
专知会员服务
91+阅读 · 2020年12月26日
【书籍推荐】简洁的Python编程(Clean Python),附274页pdf
专知会员服务
179+阅读 · 2020年1月1日
Java8 Lambda实现源码解析
阿里技术
2+阅读 · 2022年11月22日
代码重构:面向单元测试
阿里技术
0+阅读 · 2022年7月29日
Go应用单元测试实践
阿里技术
0+阅读 · 2022年4月8日
谈一谈单元测试
阿里技术
0+阅读 · 2022年2月14日
基于链路思想的SpringBoot单元测试快速写法
阿里技术
0+阅读 · 2022年1月13日
Java单元测试技巧之JSON序列化
阿里技术
0+阅读 · 2021年10月20日
wafw00f 简单利用
黑白之道
10+阅读 · 2019年6月23日
时延神经网络(TDNN)原理及其TensorFlow实现
深度学习每日摘要
56+阅读 · 2017年5月19日
国家自然科学基金
1+阅读 · 2015年12月31日
国家自然科学基金
0+阅读 · 2015年12月31日
国家自然科学基金
0+阅读 · 2014年12月31日
国家自然科学基金
0+阅读 · 2012年12月31日
国家自然科学基金
0+阅读 · 2012年12月31日
国家自然科学基金
0+阅读 · 2011年12月31日
国家自然科学基金
3+阅读 · 2011年12月31日
国家自然科学基金
0+阅读 · 2009年12月31日
国家自然科学基金
0+阅读 · 2008年12月31日
Arxiv
0+阅读 · 2022年11月22日
Arxiv
15+阅读 · 2019年6月25日
VIP会员
相关VIP内容
代码注释最详细的Transformer
专知会员服务
110+阅读 · 2022年6月30日
【经典书】概率图模型:原理与技术,1270页pdf
专知会员服务
131+阅读 · 2022年2月13日
【2021新书】面向对象的Python编程,418页pdf
专知会员服务
70+阅读 · 2021年12月15日
【干货书】面向计算科学和工程的Python导论,167页pdf
专知会员服务
41+阅读 · 2021年4月7日
专知会员服务
91+阅读 · 2020年12月26日
【书籍推荐】简洁的Python编程(Clean Python),附274页pdf
专知会员服务
179+阅读 · 2020年1月1日
相关资讯
Java8 Lambda实现源码解析
阿里技术
2+阅读 · 2022年11月22日
代码重构:面向单元测试
阿里技术
0+阅读 · 2022年7月29日
Go应用单元测试实践
阿里技术
0+阅读 · 2022年4月8日
谈一谈单元测试
阿里技术
0+阅读 · 2022年2月14日
基于链路思想的SpringBoot单元测试快速写法
阿里技术
0+阅读 · 2022年1月13日
Java单元测试技巧之JSON序列化
阿里技术
0+阅读 · 2021年10月20日
wafw00f 简单利用
黑白之道
10+阅读 · 2019年6月23日
时延神经网络(TDNN)原理及其TensorFlow实现
深度学习每日摘要
56+阅读 · 2017年5月19日
相关基金
国家自然科学基金
1+阅读 · 2015年12月31日
国家自然科学基金
0+阅读 · 2015年12月31日
国家自然科学基金
0+阅读 · 2014年12月31日
国家自然科学基金
0+阅读 · 2012年12月31日
国家自然科学基金
0+阅读 · 2012年12月31日
国家自然科学基金
0+阅读 · 2011年12月31日
国家自然科学基金
3+阅读 · 2011年12月31日
国家自然科学基金
0+阅读 · 2009年12月31日
国家自然科学基金
0+阅读 · 2008年12月31日
Top
微信扫码咨询专知VIP会员