Testing the Spring Framework

Testing the Spring Framework

Guest lecture to first year CS students at TU Delft.

D3c309254575cdef66812b8970230d2c?s=128

Stéphane Nicoll

June 20, 2019
Tweet

Transcript

  1. 1.

    © Copyright 2019 Pivotal Software, Inc. All rights Reserved. Testing

    the Spring Framework Stéphane Nicoll - @snicoll
  2. 2.

    Who am I? • Software Engineer at Pivotal (Spring Boot,

    Spring Framework and start.spring.io) • Involved in open source for 15 years (Apache Maven, Spring Framework) • snicoll on the Web (Twitter, Github) - snicoll@pivotal.io • Based in Liège, Belgium (come over, we have waffles)
  3. 3.

    Who am I? • Software Engineer at Pivotal (Spring Boot,

    Spring Framework and start.spring.io) • Involved in open source for 15 years (Apache Maven, Spring Framework) • snicoll on the Web (Twitter, Github) - snicoll@pivotal.io • Based in Liège, Belgium (come over, we have waffles)
  4. 5.

    Prerequisite “They have a good notion of Java and OOP,

    know how to use IntelliJ, are used to building tools such as Maven and Gradle, and know JUnit 5 very well. Maurício Aniche
  5. 6.

    Prerequisite “They have a good notion of Java and OOP,

    know how to use IntelliJ, are used to building tools such as Maven and Gradle, and know JUnit 5 very well. Maurício Aniche
  6. 13.

    And we had to configure the infrastructure ourselves <?xml version="1.0"

    encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jpa="http://www.springframework.org/schema/data/jpa" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/data/jpa https://www.springframework.org/schema/data/jpa/spring-jpa.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> <!-- Placeholder config --> <context:property-placeholder location="classpath:database.properties" /> <jpa:repositories base-package="com.example.cfp.domain"/> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="${jdbc.driverClassName}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="packagesToScan" value="com.example.cfp.domain"/> <property name="jpaVendorAdapter" ref="jpaVendorAdapter"/> <property name="jpaProperties"> <props> <prop key="hibernate.dialect">${hibernate.dialect}</prop> <prop key="hibernate.hbm2ddl.auto">${hibernate.hbm2ddl.auto}</prop> <prop key="hibernate.show_sql">${hibernate.show_sql}</prop> <prop key="hibernate.format_sql">${hibernate.format_sql}</prop> </props> </property> </bean> <bean id="jpaVendorAdapter" class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"> <property name="showSql" value="true"/> <property name="generateDdl" value="true"/> <property name="databasePlatform" value="org.hibernate.dialect.PostgreSQL9Dialect"/> </bean> </beans>
  7. 14.

    And we had to configure the infrastructure ourselves <?xml version="1.0"

    encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jpa="http://www.springframework.org/schema/data/jpa" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/data/jpa https://www.springframework.org/schema/data/jpa/spring-jpa.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> <!-- Placeholder config --> <context:property-placeholder location="classpath:database.properties" /> <jpa:repositories base-package="com.example.cfp.domain"/> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="${jdbc.driverClassName}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="packagesToScan" value="com.example.cfp.domain"/> <property name="jpaVendorAdapter" ref="jpaVendorAdapter"/> <property name="jpaProperties"> <props> <prop key="hibernate.dialect">${hibernate.dialect}</prop> <prop key="hibernate.hbm2ddl.auto">${hibernate.hbm2ddl.auto}</prop> <prop key="hibernate.show_sql">${hibernate.show_sql}</prop> <prop key="hibernate.format_sql">${hibernate.format_sql}</prop> </props> </property> </bean> <bean id="jpaVendorAdapter" class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"> <property name="showSql" value="true"/> <property name="generateDdl" value="true"/> <property name="databasePlatform" value="org.hibernate.dialect.PostgreSQL9Dialect"/> </bean> </beans>
  8. 15.

    And chose which libraries to use (and their versions!) <dependency>

    <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-orm</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-jpa</artifactId> <version>1.3.0.RELEASE</version> </dependency>
  9. 16.

    And chose which libraries to use (and their versions!) <dependency>

    <groupId>org.hibernate.javax.persistence</groupId> <artifactId>hibernate-jpa-2.0-api</artifactId> <version>1.0.1.Final</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-entitymanager</artifactId> <version>4.1.9.Final</version> <scope>runtime</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.6</version> <scope>runtime</scope> </dependency>
  10. 17.

    And chose which libraries to use (and their versions!) <dependency>

    <groupId>org.hibernate.javax.persistence</groupId> <artifactId>hibernate-jpa-2.0-api</artifactId> <version>1.0.1.Final</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-entitymanager</artifactId> <version>4.1.9.Final</version> <scope>runtime</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.6</version> <scope>runtime</scope> </dependency>
  11. 20.

    Summary • Conventions can significantly improve your productivity • Best

    practices for teams and the ecosystem at large • Provide sane defaults when you don’t or shouldn’t care • Make upgrade and maintenance easier • …. but shouldn’t get in your way • Built on top of an API you could use to implement something slightly or totally different • Can be customised without having you to redefine everything • Depending on the scope, not everything can be the target of a convention
  12. 26.

    Unit tests should not require any “framework” @Service public class

    SubmissionService { @Autowired private SubmissionRepository submissions; @Transactional public Submission create(SubmissionRequest request) { Submission submission = new Submission(); ... return this.submissions.save(submission); } }
  13. 27.

    Unit tests should not require any “framework” @Service public class

    SubmissionService { private final SubmissionRepository submissions; public SubmissionService(SubmissionRepository submissions) { this.submissions = submissions; } @Transactional public Submission create(SubmissionRequest request) { Submission submission = new Submission(); ... return this.submissions.save(submission); } }
  14. 28.

    Unit testing a client for an HTTP API class SpringBootMetadataReaderTests

    { private final RestTemplate restTemplate = new RestTemplate(); private final MockRestServiceServer server = MockRestServiceServer.bindTo( this.restTemplate).build(); @Test void readAvailableVersions() throws IOException { this.server.expect(requestTo("https://spring.io/project_metadata/spring-boot")) .andRespond(withSuccess(new ClassPathResource( "metadata/sagan/spring-boot.json"), MediaType.APPLICATION_JSON)); ... this.server.verify(); } }
  15. 31.
  16. 33.

    Slice test example @RestClientTest(SpringBootMetadataReader.class) class SpringBootMetadataReaderTests { @Autowired private SpringBootMetadataReader

    springBootMetadataReader; @Autowired private MockRestServiceServer server; @Test void readAvailableVersions() throws IOException { this.server.expect(requestTo("https://spring.io/project_metadata/spring-boot")) .andRespond(withSuccess(new ClassPathResource( "metadata/sagan/spring-boot.json"), MediaType.APPLICATION_JSON)); ... this.server.verify(); } }
  17. 34.

    Slice test example with @MockBean @WebMvcTest(controllers = CfpController.class) class CfpControllerTest

    { @Autowired private MockMvc mvc; @MockBean private SubmissionService submissionService; @Test void submitTalk() throws Exception { given(this.submissionService.create(any())).willReturn(new Submission()); this.mvc.perform(post("/submit") .param("title", "Alice in Wonderland") .param("summary", "my abstract")) .andExpect(status().isFound()) .andExpect(header().string(HttpHeaders.LOCATION, "/submit?navSection=submit")); verify(this.submissionService).create(any()); } }
  18. 36.

    testcontainers example @DisabledWithoutDockerTestcontainers public class ReactiveRestClientAutoConfigurationTests { @Container static ElasticsearchContainer

    elasticsearch = new ElasticsearchContainer(); private ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of( ReactiveRestClientAutoConfiguration.class)); @Test void restClientCanQueryElasticsearchNode() { String endpoint = elasticsearch.getContainerIpAddress() + ":" + elasticsearch.getFirstMappedPort(); this.contextRunner.withPropertyValues( "spring.data.elasticsearch.client.reactive.endpoints=" + endpoint) .run((context) -> { ReactiveElasticsearchClient client = context.getBean( ReactiveElasticsearchClient.class); ... }); } }
  19. 38.

    ApplicationContextRunner • Test utility to prepare a mini ApplicationContext suitable

    for a very precise scenario: • Specific auto-configuration(s) • User configuration, if any • Environment tuning (properties customization) • System properties handling • Starts/Stop the context automatically with a ContextConsumer callback • AssertJ style assert • hasSingleBean, doesNotHaveBean, hasFailed • getBean, getBeans, etc
  20. 39.

    ApplicationContextRunner example @Test void shouldFailWhenNativeTypesAreNotAvailable() { this.contextRunner .run((context) -> {

    assertThat(context).hasFailed(); assertThat(context.getStartupFailure()) .hasRootCauseInstanceOf( NativeTypesNotAvailableException.class); }); }
  21. 40.

    ApplicationContextRunner example @Test void shouldFailWhenNativeTypesAreNotAvailable() { this.contextRunner .withConfiguration(AutoConfigurations.of( Neo4jDataAutoConfiguration.class, TransactionAutoConfiguration.class))

    .run((context) -> { assertThat(context).hasFailed(); assertThat(context.getStartupFailure()) .hasRootCauseInstanceOf( NativeTypesNotAvailableException.class); }); }
  22. 41.

    ApplicationContextRunner example @Test void shouldFailWhenNativeTypesAreNotAvailable() { this.contextRunner .withConfiguration(AutoConfigurations.of( Neo4jDataAutoConfiguration.class, TransactionAutoConfiguration.class))

    .withPropertyValues( "spring.data.neo4j.uri=bolt://localhost:7687", "spring.data.neo4j.use-native-types:true") .run((context) -> { assertThat(context).hasFailed(); assertThat(context.getStartupFailure()) .hasRootCauseInstanceOf( NativeTypesNotAvailableException.class); }); }
  23. 42.

    ApplicationContextRunner example @Test void shouldFailWhenNativeTypesAreNotAvailable() { this.contextRunner .withConfiguration(AutoConfigurations.of( Neo4jDataAutoConfiguration.class, TransactionAutoConfiguration.class))

    .withPropertyValues( "spring.data.neo4j.uri=bolt://localhost:7687", "spring.data.neo4j.use-native-types:true") .withClassLoader(new FilteredClassLoader( "org.neo4j.ogm.drivers.bolt.types")) .run((context) -> { assertThat(context).hasFailed(); assertThat(context.getStartupFailure()) .hasRootCauseInstanceOf( NativeTypesNotAvailableException.class); }); }
  24. 43.

    Classpath tuning @RunWith(ModifiedClassPathRunner.class) @ClassPathOverrides("javax.servlet:servlet-api:2.5") public class NoSuchMethodFailureAnalyzerTests { @Test public

    void noSuchMethodErrorIsAnalyzed() { Throwable failure = createFailure(); assertThat(failure).isNotNull(); FailureAnalysis analysis = new NoSuchMethodFailureAnalyzer() .analyze(failure); assertThat(analysis).isNotNull(); ... } private Throwable createFailure() { ... } }
  25. 44.

    Links • https://spring.io/projects/spring-boot • https://start.spring.io • https://spring.io/guides • https://concourse-ci.org/ •

    Testing Spring Boot Applications (Andy Wilkinson @ Spring I/O 2019): https:// www.youtube.com/watch?v=5sjFn9BsAds