stripe / stripe-java

Java library for the Stripe API.

Home Page:https://stripe.com

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Support mocking of stripe outbound RPCs for unit tests

Kurru opened this issue · comments

commented

Is your feature request related to a problem? Please describe.

When writing unit tests, there is a need to avoid outbound RPCs to another server. Today the Stripe API uses static methods and global (or request) config parameters, but there does not seem to be a way to mock the request/verify the request without running StripeMock, which is perhaps too large for a java unit test.

Describe the solution you'd like

If there was a constructor/factory based approach to constructing the stripe api, then we would be able to mock this away in our DI layer.

Today

Customer.create(customerCreateParamsObj);

Desirable alternatives

stripeFactoryObj.customerService().create(customerCreateParamsObj);

In my code I would likely provide CustomerService via DI, and then call like such:

customerService.create(customerCreateParamsObj);

This way I would be able to provide a Mockito implementation for CustomerService and setup mocks/verifications.

Describe alternatives you've considered

My solution until this is supported will be to wrap every Stripe API in a class/method and indirectly call the stripe API.

Alternatively it seems clients could use Mockito to mock static methods, though this approach is less desirable.

Additional context

No response

Hello @Kurru,

I totally agree that the static implementation of the stripe API is for unit testing a pain and addition to that not the java way. I was surprised, that the stripe-node implementation, with out looking to deep to the code, creates objects for it (see https://github.com/stripe/stripe-node#usage-with-typescript).

But nevertheless, here is an example how you can mock with mockito the stripe API:

    @Test
    void createCustomer() {
        try (MockedStatic<Customer> customerMockStatic = Mockito.mockStatic(Customer.class)) {
            Customer customerMock = mock(Customer.class);
            customerMockStatic.when(() -> Customer.create(Map.of("email", "email", "metadata", Map.of("key1", "value1")))).thenReturn(customerMock);

            Customer customer = stripeBillingRepository.createCustomer("email", Map.of("key1", "value1"));

            assertEquals(customerMock, customer);
        }
    }

There is also another alternative (but not tested from me): #93 (comment)

Hello @Kurru, thank you for filing this.

You are right, this is a shortcoming of the library. We call your constructor/factory-based approach a "services-based architecture" and it is something we do intend to add to stripe-java, and are actively designing. We did something similar for stripe-php and stripe-java will likely be next.

I'll leave this open, mark as "future" and we'll be sure to post updates about our progress here.

If you have any advice about the design, or examples of other libraries with this architecture that you think are designed well, we appreciate any advice or input!

commented

Some typed client libraries I've used recently:

Some other features you may want to consider

  • Builders for domain classes (like Customer) to enable types in tests more easily (instead of long chains of .setXYZ())
  • RPC interceptors to enable global concerns such as monitoring/tracing/logging integrations. Allow users to instrument outbound RPCs to Stripe for monitoring and debug purposes. Alerts, dashboards, AWS X-Ray etc. See GRPC and AWS client libraries for examples
  • Support async RPCs in addition to blocking interfaces (like gRPC and AWS API's support). 2nd set of APIs return CompletableFuture<YourResponseObject>
commented

Additionally, the request objects don't have equality defined, which makes testing harder

commented

New discoveries, timestamps exist, but don't document they are second granularity and are simple Long's. Returning java.time.Instant would be preferred.

commented

Consider adding types for status's. For example, Invoice.status is a string, but really an enum. Consider providing a typed interface for this field.