camunda-community-hub / spring-zeebe

DEPRECATED. Easily use the Zeebe Java Client in your Spring or Spring Boot projects
Apache License 2.0
205 stars 118 forks source link

Relax dependency on ZeebeClientImpl #107

Closed ajeans closed 3 years ago

ajeans commented 3 years ago

Following 0d5c20d490394af4400297024ab8215ca15857ae and its use of the Interface rather than the Implementation, it is now possible to kill another direct Impl dependency in ZeebeClientSpringConfiguration

From

  @Bean
  public ZeebeClientObjectFactory zeebeClientObjectFactory() {
    return () -> (ZeebeClientImpl) zeebeClientBuilder.build();
  }

To

  @Bean
  public ZeebeClientObjectFactory zeebeClientObjectFactory() {
    return () -> zeebeClientBuilder.build();
  }

As we currently mock zeebeClient with mockito for some of our unit tests, this would make mocking even easier for us (No need for mockito-inline)

salaboy commented 3 years ago

@ajeans yes.. that make sense to me, would you submit a PR for it?

ajeans commented 3 years ago

I was asked to provide more information about how we mock zeebe for unit testing

Assuming a spring boot app that will need to rely on a ZeebeClient for some services, and the requirement to unit test the services with Zeebe Assuming that we want UT with no external dependency and fast (so no existing Zeebe gateway / broker, no test-container)

  1. Make your spring boot app depend on 'io.zeebe.spring:spring-zeebe' (NOT 'io.zeebe.spring:spring-zeebe-starter')
  2. Make your tests depend on 'org.mockito:mockito-inline' and 'org.springframework.boot:spring-boot-starter-test'
  3. Annotate your spring boot app with
    @EnableZeebeClient
    @ZeebeDeployment(classPathResources = {"example.bpmn"})
    @Import(ZeebeBrokerConfig.class)
    @SpringBootApplication
  4. Create a spring @Configuration called ZeebeBrokerConfig that will basically expose a ZeebeClientBuilder in two different ways

    @ConditionalOnProperty(
      prefix = ZEEBE_GATEWAY_PREFIX,
      name = ZEEBE_GATEWAY_MOCK_KEY,
      havingValue = "false",
      matchIfMissing = true)
    @Bean
    public ZeebeClientBuilder zeebeClientBuilder() {
    LOGGER.info("Loading zeebe clients connected to a zeebe broker");
    final ZeebeClientBuilderImpl builder = new ZeebeClientBuilderImpl();
    builder.withProperties(extractPropertiesFromEnvironment());
    LOGGER.info("Zeebe broker client configuration {}", builder.toString());
    return builder;
    }
    
    @ConditionalOnProperty(
      prefix = ZEEBE_GATEWAY_PREFIX,
      name = ZEEBE_GATEWAY_MOCK_KEY,
      havingValue = "true")
    @Bean
    public ZeebeClientBuilder mockZeebeClientBuilder() {
    LOGGER.warn("Loading zeebe clients connected to MOCK");
    return new MockZeebeClientBuilder();
    }
  5. For the non mock side, we need to forward properties from the spring environment manually into the ZeebeClientBuilderImpl Code looks like the following for my use case, but is very (too) specific to the way you will declare sources of properties in your environment
    private Properties extractPropertiesFromEnvironment() {
    final Properties properties = new Properties();
    for (final PropertySource<?> propertySource :
        ((StandardEnvironment) environment).getPropertySources()) {
      if (propertySource instanceof EnumerablePropertySource) {
        Arrays.stream(((EnumerablePropertySource) propertySource).getPropertyNames())
            .forEach(
                propertyName ->
                    properties.put(propertyName, propertySource.getProperty(propertyName)));
      }
    }
    return properties;
    }
  6. MockZeebeClientBuilder

    public class MockZeebeClientBuilder implements ZeebeClientBuilder {
    [...]
    // Lots of things you will need to override to `return this;`
    [...]
    // Implement the build() with enough mockito to get your app to start
    @Override
    public ZeebeClient build() {
    final ZeebeClientImpl zeebeClient = BDDMockito.mock(ZeebeClientImpl.class); // :throwup:
    
    final DeployWorkflowCommandStep1 workflowCommandStep1 =
        BDDMockito.mock(DeployWorkflowCommandStep1.class);
    
    BDDMockito.when(zeebeClient.newDeployCommand()).thenReturn(workflowCommandStep1);
    
    final DeployWorkflowCommandStep1.DeployWorkflowCommandBuilderStep2 workflowCommandStep2 =
        BDDMockito.mock(DeployWorkflowCommandStep1.DeployWorkflowCommandBuilderStep2.class);
    
    BDDMockito.when(workflowCommandStep1.addResourceFromClasspath(BDDMockito.anyString()))
        .thenReturn(workflowCommandStep2);
    
    final ZeebeFuture<DeploymentEvent> future = BDDMockito.mock(ZeebeFutureDeployment.class);
    
    BDDMockito.when(workflowCommandStep2.send()).thenReturn(future);
    
    final DeploymentEvent deploymentEvent = BDDMockito.mock(DeploymentEvent.class);
    
    BDDMockito.when(future.join()).thenReturn(deploymentEvent);
    
    CreateWorkflowInstanceCommandStep1 workflowStep1 =
        BDDMockito.mock(CreateWorkflowInstanceCommandStep1.class);
    BDDMockito.when(zeebeClient.newCreateInstanceCommand()).thenReturn(workflowStep1);
    
    CreateWorkflowInstanceCommandStep1.CreateWorkflowInstanceCommandStep2 workflowStep2 =
        BDDMockito.mock(
            CreateWorkflowInstanceCommandStep1.CreateWorkflowInstanceCommandStep2.class);
    BDDMockito.when(workflowStep1.bpmnProcessId(BDDMockito.anyString())).thenReturn(workflowStep2);
    
    CreateWorkflowInstanceCommandStep1.CreateWorkflowInstanceCommandStep3 workflowStep3 =
        BDDMockito.mock(
            CreateWorkflowInstanceCommandStep1.CreateWorkflowInstanceCommandStep3.class);
    BDDMockito.when(workflowStep2.latestVersion()).thenReturn(workflowStep3);
    
    ZeebeFuture<WorkflowInstanceEvent> zeebeFutureWorkflow =
        BDDMockito.mock(ZeebeFutureWorkflow.class);
    BDDMockito.doReturn(workflowStep3).when(workflowStep3).variables(BDDMockito.anyMap());
    BDDMockito.when(workflowStep3.send()).thenReturn(zeebeFutureWorkflow);
    
    WorkflowInstanceEvent workflowInstance = BDDMockito.mock(WorkflowInstanceEvent.class);
    BDDMockito.when(zeebeFutureWorkflow.join(BDDMockito.anyLong(), BDDMockito.eq(TimeUnit.SECONDS)))
        .thenReturn(workflowInstance);
    
    return zeebeClient;
    }

So this is quite convoluted at the moment. Hope this helps

This issue and the associated will only allow us to relax the dependency from mockito-inline to mockito

salaboy commented 3 years ago

@ajeans this should be included in the 0.26.0 release now in maven central.

ajeans commented 3 years ago

@salaboy Yes that works fine, I am now on 0.26.2 and I ditched mockito-inline with a wide grin.

Thanks :+1: