TL;DR
Your tests are malformed. There are several ways to tackle this, based on context that is not fully clear. I'm going to answer with the generally most applicable approaches. Not all of them will apply to your specific case but (a) I can't conclusively judge which it is and (b) it's still useful information on how to structure your tests in general.
When reading a test for the first time, I read it bottom to top so that I can understand, in logical order:
- What is this test trying to assert? (green)
- Where did that behavior come from? (purple)
- What was needed for this behavior to occur? (blue)
Applied to your test, this is what my mental picture looks like:
The key thing to notice here is that step 2 answers what the unit under test is, which in this case is the password generator, not the dialog. The only thing the dialog is doing in this test is providing access to a generator that you seem to be unable to otherwise access. This suggests tight coupling between the dialog and generator that could be avoided with proper use of dependency injection.
But before I get ahead of myself, the core issue here is that your approach muddies the lines between what should be very different scenarios, and the correct test structure depends on which scenario it is.
Pick the below option based on the title that best represents how you think about this domain/space. I'll point out specifically what part of your current test creates friction with that interpretation, and how to resolve it.
I suggest reading them all to get the full picture, but I suspect option 3 is going to be the one you end up picking.
Option 1 - Dialog and generator each have their own independently defined responsibility. Dialog represents the UI, generator implements the needed password generation logic.
Main thing to fix:
Separate your dialog/generator unit tests, rely on dependency injection to compose the two components, use a mocked generator in the dialog unit tests
This is the straightforward dependency injection approach that is most commonly used. The classes are defined as their own thing, with their own reasons for existing, and their own unit test suite surrounding them.
You should be able to independently develop the generator, including test suite, before the dialog even exists. The dialog, while it depends on the existence of a generator, can be developed and tested with a mocked generator. When composing the two (during runtime), rely on dependency injection (which is generally done via a DI container, I'm not familiar enough with Java to know which one is generally used).
That's the general advice, but your case is slightly more nuanced, as evidenced by your comment here.
You don't need a single instance of a generator that's tied to the dialog, you need the dialog to be able to generate several instances. I'm not sure if that assertion is correct or not (not enough context for me to decide), and I'm wondering whether you could reasonably make do with a single instance whose parameters are mutable, rather than needing to generate immutable instances on the fly.
I don't want to dig into a philosophical discussion on the necessity of immutability. I'll boil it down to this: if you were to take the mutable route, you could significantly simplify this by relying on straightforward dependency injection of a single generator instance. I suggest avoiding additional complexity unless you have a good enough justification for it.
Option 2 - Dialog's responsibility is to return a password. The fact that this is done via a generator is a private implementation detail.
Main thing to fix:
Dialog unit tests should not be actively aware of the existence of a password generator. Dialog public interface can't return/reference generator class in any way. Dialog should be returning a password (string), and you should write your assertions on that string value.
This is a bit of a variation on the above. Some "private implementation details" are still injected dependencies, because the "detail" is actually complex enough to warrant having its own class and test suite.
However, there are also cases where you might create a private (nested) class that performs a small duty, not enough for it to be considered a component but rather to keep it hidden as a specific implementation of this class.
If this is the case, then for all intents and purposes everyone except the dialog should not in any way be aware of the existence of this generator class. Unit tests focus on public behavior, and that does not include private implementation details.
What I would expect to see in your test is that the dialog return a password, not a generator, and then you can perform your test assertions on that returned password.
Option 3 - Dialog's responsibility is that of a "generator factory", i.e. it provides access to a generator, it doesn't use make us of it itself.
Main thing to fix:
Separate the dialog from the factory. Refer back to option 1, with the only difference that the generator factory is injected instead of the generator itself.
Based on your comment, I suspect this is the most applicable scenario for you.
Just to be clear here, the above title is phrasing it how you're currently approaching it, not how I think it could be better approached.
A factory is a valid pattern, but it is a decidedly different responsibility from what a "dialog" handles. A dialog is a UI component, a factory is an internal tool that helps you generate instances of another class based on your on-the-fly needs. Making the dialog also a factory is a violation of SRP. These need to be two separate classes.
For the purpose of what I'm trying to convey here, I don't care whether you use a factory pattern or a builder pattern (which is effectively just a method-chained fluent factory pattern anyway), although I would suggest having a reusable builder (i.e. something that can repeatedly build new instances). I'm going to refer to it as a factory from this point on for brevity.
The key friction I see in your code is that the dialog contains a tight coupling to the static string generator builder. If you instead inject that as a factory, that issue gets resolved.
You might wonder why I'm arguing against tight coupling between the dialog/generator, and am not similarly opposed to the same tight coupling between the factory/generator. The short and simple answer is that the very nature of a factory makes it tightly coupled to the product that it creates. It has to be innately aware of the product's type and constructor argument in order to make it make sense.
From a dependency perspective, the factory and the product are a package deal. You can still write unit tests for the product without including the factory, but at the same time you don't really need to write tests for the factory with a mocked product, since the factory tends to be in charge of instantiating the product (meaning you can't mock it). And that's fine, on the basis of what a factory is.
From this point on, a lot of the advice from option 1 starts applying here again, except that it applies to the generator factory instead of the generator itself.
new PasswordDialog(new PasswordGenerator(...))
?withMinLength()
when building the generator