Practical guide
The test that prevents a failed Refresh from clearing the cache
A contract test for an easy-to-break guarantee: if the source fails during Refresh, the last valid value must remain available.
A cache already contains version v1 of a profile. It is time to refresh it, but the source does not respond. What should the next Get return?
The method signatures do not decide the answer:
Refresh(ctx context.Context, key string) (Value, error)
Get(key string) (Value, bool)There are at least two possible behaviors:
Refreshfails and removesv1;Refreshfails and preservesv1as the last valid value.
If consumers need to keep operating with older data during a transient failure, the second behavior is a cache guarantee. It should be written down and it should have a test.
The incomplete test that looks sufficient#
This case verifies that the error crosses the API:
func TestRefreshReturnsSourceError(t *testing.T) {
source := &stubSource{err: ErrUnavailable}
cache := NewCache(source)
_, err := cache.Refresh(context.Background(), "profile:42")
if !errors.Is(err, ErrUnavailable) {
t.Fatalf("expected source error, got %v", err)
}
}It is correct, but it does not test the important guarantee. An implementation can return that exact error after deleting a valid value. The test passes while the consumer loses its fallback.
Establish the previous state and observe the next one#
The complete case first establishes a successful value, triggers the failure, and queries the cache again through its public API:
func TestFailedRefreshPreservesPreviousValue(t *testing.T) {
ctx := context.Background()
source := &stubSource{value: Value("v1")}
cache := NewCache(source)
if _, err := cache.Refresh(ctx, "profile:42"); err != nil {
t.Fatalf("seed cache: %v", err)
}
source.err = ErrUnavailable
if _, err := cache.Refresh(ctx, "profile:42");
!errors.Is(err, ErrUnavailable) {
t.Fatalf("expected source error, got %v", err)
}
got, ok := cache.Get("profile:42")
if !ok {
t.Fatal("expected previous value to remain cached")
}
if got != Value("v1") {
t.Fatalf("expected v1, got %q", got)
}
}The stub controls a necessary condition: the source response. The assertions, however, concern what a consumer can observe from Cache: the Refresh error and the value returned by the following Get.
The incorrect implementation it detects#
This order may look reasonable if someone thinks refresh means clear and load again:
func (c *Cache) Refresh(ctx context.Context, key string) (Value, error) {
delete(c.items, key)
value, err := c.source.Load(ctx, key)
if err != nil {
return "", err
}
c.items[key] = value
return value, nil
}The error-only test accepts it. TestFailedRefreshPreservesPreviousValue fails because the second Get can no longer find v1.
An implementation that meets the guarantee retrieves the new value first and replaces the old one only after success:
func (c *Cache) Refresh(ctx context.Context, key string) (Value, error) {
value, err := c.source.Load(ctx, key)
if err != nil {
return "", err
}
c.items[key] = value
return value, nil
}The test does not require this algorithm. Another implementation could use versions, a transaction, or an atomic swap. They all pass if they preserve the same public observation.
The reusable template#
To test a preservation guarantee under failure:
- create a valid state through the public API;
- configure the dependency to fail;
- execute the operation that could replace or destroy the state;
- assert both the error and the state that remains visible.
The same shape applies to configuration that must not be replaced when invalid, a synchronization that must not delete the last useful copy, or a credential renewal that must not remove current credentials before obtaining new ones.
The decisive question for the test is: which plausible incorrect implementation stops passing? Here the answer is concrete: one that deletes the previous value before it knows whether a replacement is available.
The testability foundation explains why an observable guarantee lets a test require behavior without freezing the internal path.
Do not test only that the operation failed; test which promise still holds after the failure.