My first inclination is not to mock this: It looks like startAsync is a part of MyClassImplementRunner's public API, and that you should be testing these pieces together. In a test class like MyClassImplementRunnerTest, it makes sense to treat the system under test as MyClassImplementRunner without trying to split it up. Otherwise, it's very easy to lose track of what you're testing, including what is real versus what is a mock.
If there is any external condition that MyClassImplementRunner is looking for, you can mock that dependency, which would likely cause your CompletableFuture to return immediately; however, you've only shown us a single String parameter.
That said, it's possible that startAsync contains logic that you'd like to test exhaustively without a real MyClassImplementRunner. In that case, you could create an overload for testing, possibly with limited visibility or test-only annotations to indicate that it shouldn't be called in production.
public static CompletableFuture<Void> startAsync(String param1) {
return startAsync(new MyClassImplementRunner(param1);
}
/** Package-private, for a test class in the same package. */
@VisibleForTesting static CompletableFuture<Void> startAsync(Runnable task) {
return CompletableFuture.runAsync(task).whenComplete(
(response, throwable) -> {
//some code when complete
});
}
By splitting this up, you can now run startAsync(new Runnable()) in tests to simulate an instantly-succeeding task, and run startAsync(() -> { throw new RuntimeException(); }) to simulate an instantly-failing task. This allows you to test startAsync independently from MyClassImplementRunner.
It may not seem wise to refactor for testing or to introduce test-only methods, and this is a fair assessment: Purely speaking, MyClassImplementRunner should be tested exactly as consumers would run it, without mocking. However, if you're saying it is much more convenient in tests to run with a different Runnable than MyClassImplementRunner, you are in control of the code and you can prepare for this by including the appropriate flexiblity ("testing seam") in the code you control. In fact, if startAsync is a separate-enough method that it can take an arbitrary Runnable, you may choose to separate it out to be a separate method with separate testing.