1

I need to generate OpenApi documentation as a file. Technically this should be possible without any real services (i've got this working) and data base connection.

The "test slice" @WebMvcTest is not working so i have to manually cut a Spring context:

@SpringBootTest //load complete Spring context
@AutoConfigureMockMvc //configure MockMvc to download the OpenAPI documentation
@MockBean({DeviceUpdateService.class,
        ...,
        
        SomeConfiguration.class})
class GenerateApiDocTest extends PostgresBaseTest {

This works fine but i still need to provide a database - here by extending PostgresBaseTest (using test containers to start a DB).

So what to mock to get rid of Error creating bean with name 'entityManagerFactory' defined in class path resource or the need of extending PostgresBaseTest. ? I need to mock the complete JPA/JDBC bootstrapping - but i don't know with what class it starts (if there is one to switch it off)

(I can use test containers - as i use in my DB tests but i do not want to provide it here.)

Or is there a better way to only provide things needed for OpenApi generation?

dermoritz
  • 12,519
  • 25
  • 97
  • 185
  • @DataJpaTest should do the trick. – Mar-Z Mar 28 '23 at 14:59
  • no - it has a similar problem as WebMvcTest - it is missing specific beans for openApi generation. or please provide a full example using JpaTest (all controllers must be manually added here at least) – dermoritz Mar 28 '23 at 15:07
  • Using unit test you want to validate openapi document generation or you want to store the document to specific format? – Dhaval Gajjar Apr 19 '23 at 11:55
  • i just want to store the file during build - but i don't want to start the whole application for it - just the stuff needed. as said my test is running fine but the "test frame" is too large – dermoritz Apr 19 '23 at 15:32

5 Answers5

4

If anyone looks for a SpringBoot Webflux solution with a reactive database here you go:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
// add here your repositories, configurations and controller dependencies
@MockBean({KeyVaultRepository.class, StorageClientImpl.class, ....})
// remove or add LiquibaseAutoConfiguration/FlywayAutoConfiguration etc. if necessary
@EnableAutoConfiguration(exclude = {R2dbcAutoConfiguration.class, DataSourceAutoConfiguration.class, LiquibaseAutoConfiguration.class})
class OpenApiTest {

    private static final String PATH_TO_SAVE_JSON_FILE = "./open-api.json";
    private static final String ROUTE_TO_API_DOCS = "http://localhost:8080/v3/api-docs";

    @Test
    void getApiDocs() throws Exception {
        val contentAsString = getWebClient().get().retrieve().bodyToMono(String.class).block();

        val path = Paths.get(PATH_TO_SAVE_JSON_FILE);
        val strToBytes = contentAsString.getBytes();

        Files.write(path, strToBytes);
    }

    private static WebClient getWebClient() {
        return WebClient.create(ROUTE_TO_API_DOCS);
    }
}

with these dependencies:

implementation 'org.springdoc:springdoc-openapi-starter-webflux-api:2.1.0'
implementation 'org.springdoc:springdoc-openapi-starter-common:2.1.0'
implementation 'org.springdoc:springdoc-openapi-webflux-ui:1.7.0'
implementation 'org.springdoc:springdoc-openapi-webflux-core:1.7.0'
Anna Klein
  • 1,906
  • 4
  • 27
  • 56
3

If the datasource is auto-configured, it can be excluded for tests by excluding the DataSourceAutoConfiguration class. A line like below would need to be added to the test:

@EnableAutoConfiguration(exclude = DataSourceAutoConfiguration.class)

Might also need to move the @EnableJpaAuditing annotation, which I assume is causing the Error creating bean with name 'jpaAuditingHandler' exception, to a separate @Configuration class that can be conditionally skipped

package io.github.devatherock.config;

import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@Configuration
@EnableJpaAuditing
@ConditionalOnProperty(name = "jpa.enabled", matchIfMissing = true)
public class JpaConfig {

}

Now the custom JpaConfig class can be disabled in the api spec generation test by setting jpa.enabled=false

@TestPropertySource(properties = "jpa.enabled=false")

A complete minimal test(written in Spock), that generates an api spec while excluding datasource configuration and disabling JPA auditing, is below:

package io.github.devatherock.controller

import com.fasterxml.jackson.databind.ObjectMapper
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.context.TestPropertySource
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
import spock.lang.Specification

import java.nio.file.Files
import java.nio.file.Paths

@SpringBootTest
@TestPropertySource(properties = "jpa.enabled=false")
@AutoConfigureMockMvc
@EnableAutoConfiguration(exclude = DataSourceAutoConfiguration.class)
class GenerateApiDocSpec extends Specification {

    @Autowired
    MockMvc mockMvc

    @Autowired
    ObjectMapper objectMapper

    void 'test generate api doc'() {
        setup:
        Files.deleteIfExists(Paths.get('api-spec.json'))

        when:
        String apiSpec = mockMvc.perform(MockMvcRequestBuilders.get('/v3/api-docs'))
                .andReturn().response.contentAsString

        then:
        def json = objectMapper.readValue(apiSpec, Map)
        json['paths']['/hello']

        cleanup:
        new File('api-spec.json') << apiSpec
    }
}

The complete application and tests can be found on github

devatherock
  • 2,423
  • 1
  • 8
  • 23
  • it does not work, i tried multiple ways. first with your setup i can not MockMvc. On the other hand just adding "@EnableAutoConfiguration(exclude = DataSourceAutoConfiguration.class)" to get rid of JPA stuff didn't helped i see "Error creating bean with name 'jpaAuditingHandler': Cannot resolve reference to bean 'jpaMappingContext'" – dermoritz Apr 25 '23 at 13:23
  • I had used `TestRestTemplate` instead of `MockMvc` as either would have worked for this use case. Anyway, now I have edited the answer to use MockMvc. As for the other errors, your code seems to be using additional JPA related beans. If you can provide a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) with the JPA/datasource related beans that your code initializes/uses, it'll be helpful to figure out which other beans need to be mocked – devatherock Apr 25 '23 at 13:52
  • thanks for all the effort you put in here! i think it is related to @EntityListeners(AuditingEntityListener.class) on one of the entities providing "@CreatedDate" on a ZonedDateTime field. i also found this https://stackoverflow.com/a/30597861/447426 but adding these other classes to exclude didn't helped (still error regarding auditing bean) – dermoritz Apr 25 '23 at 15:00
  • I was able to recreate the JPA exceptions after adding the `@EnableJpaAuditing` annotation. I have updated the answer with a workaround for that – devatherock Apr 25 '23 at 22:14
  • this is working - accepted and voted up - thanks – dermoritz Apr 26 '23 at 08:08
  • another way (without changing other code than test) is to declare a inner "@Configuration" class with same bean but @Primary - at the end this Auditing bean was indded the cause for the trouble – dermoritz Apr 26 '23 at 08:31
0

If you use SpringDoc as a framework for OpenAPI generation, you could write a test in order to save api-spec.json into a file.

@ActiveProfiles("test")
@SpringBootTest
public class ApiSpecJsonFileExtractor {
    @Value("${extraction.api-spec.json}")
    String filename;
    @Value("${springdoc.api-docs.path}")
    String apiDocJsonPath;

    @Test
    void extractApiSpecJsonFile() throws Exception {
        File file = new File(filename);
        Path filePath = file.toPath();
        if (file.exists()) {
            Assertions.assertThat(file.isFile()).isTrue();
        } else {
            Path path = file.getParentFile().toPath();
            if (Files.notExists(path)) {
                Files.createDirectory(path);
            }
            if (Files.notExists(filePath)) {
                Files.createFile(file.toPath());
            }
        }
        mockMvc.perform(MockMvcRequestBuilders.get(apiDocJsonPath))
                .andDo(result -> Files.write(file.toPath(), result.getResponse().getContentAsString().getBytes()));
    }

And I recommend you to separate OpenAPI annotated interfaces from the business code (classes) into another module (gradle, i.e.). You can create simple App into that module to untie your OpenAPI application from business properties.

bvn13
  • 154
  • 11
  • the problem is that "@SpringBootTest" starts the full spring context including all beans, this is exactly what i not want. my test is already running fine - but it takes to long to start due to too much stuff in context – dermoritz Apr 19 '23 at 15:30
  • @dermoritz I understand you. Unfortunately, SpringDoc needs Spring MVC to be started. Therefore all your beans participating in MVC must start. I guess the best option is to move SpringDoc description into separated module in order to decouple it from all MVC beans. – bvn13 Apr 20 '23 at 07:43
  • as you see in my example - in fact it is enough to provide empty mocks for all of this, but what to mock in regards of persistence layer? – dermoritz Apr 20 '23 at 12:36
0

if you want to generate documentation without a database connection then you can use tools like SpringDoc or springfox. These tools generate the OpenAPI documentation by scanning your spring application

  1. You can add Dependency into your pom.xml OR
  2. Create a Spring configuration class that defines your API endpoint and spring beans which are needed. OR
  3. Add some annotation to your spring application class like @EnableOpenApi which tells SpringDoc to scan your application

after adding any one Start your application and http://localhost address/swagger-ui.html to view the generated OpenAPI documentation.

hope this one helps you!

Swapnil
  • 94
  • 1
  • 12
0

You can use MockMVC without the need for a Spring Test by creating a standalone setup. Here's how:

class GenerateApiDocTest
{
  private MockMvc mockMvc;

  @InjectMocks
  private GeneralApiDoc sut;

  @Mock
  private DeviceUpdateService deviceUpdateService;

  @BeforeAll
  public void setup(){
    mockMvc = MockMvcBuilders.standaloneSetup(sut).build();
    doNothing().when(deviceUpdateService.updateDevice());
  }

  @Test
  public void testMvc() throws Exception
  {
    mockMvc.perform(MockMvcRequestBuilders.get("http://google.com")).andExpect(status().isOk());
  }
}

Notice there are no need for any extra annotations. This is done with JUnit5 and Mockito. Very basic test that will accept normal mocking and spying but still tests the service layer. You can add options to the mockMvc as well, such as MockMvcBuilders.standaloneSetup(sut).setControllerAdvice(new MyAdvice()).build(); to test exception handling as well.

tbatch
  • 1,398
  • 10
  • 21