(maint) Pull apart test for reuse

The cache expiration service test had some useful parts in it that will
be useful for another test later. This pulls it apart so that those
pieces are available and also is a little refactoring so that I can
become familiar with how it was working.
(maint) Unify many directory structures for test

The previous tests used a lot of different structures and set them up
individually. This was nice in that each test was self contained, but it
also left a lot of noise in some tests that only cared that *any*
environment simply exists, and the exact form of the environments wasn't
important. This unifies all of those cases so that there is a reusable
"basic" environment available.
(PUP-3621) Ensure that get! caches

The introduction of get! caused any users of that method to circumvent
the cache. This happened because, rather than delegating to get() and
raising an error, it implemented similar logic to get(), but only from
the common base class. This caused it to skip all caching checks. This
adds caching back in and adds tests for that behavior.
(maint) Remove inheritance to make mistakes less likely

The previous mistake happened partly because the classes were written
with inheritance rather than delegation. The inheritance caused the
Combined#get! implementation to be used, which was appropriate for it,
but not for the Cached get. The Cached implementation also has no reason
to allow multiple loaders, instead it only needs a single loader and
multiple ones can be put together using the Combined loader. This change
makes that distinction much clearer.
Merge pull request #3291 from zaphod42/issue/stable/pup-3621-lost-env-caches

(PUP-3621) Ensure that get! caches