cache filter: mock cache in CacheFilterTest#20593
cache filter: mock cache in CacheFilterTest#20593mkbehr wants to merge 7 commits intoenvoyproxy:mainfrom
Conversation
Signed-off-by: Michael Behr <mkbehr@google.com>
Signed-off-by: Michael Behr <mkbehr@google.com>
|
Presubmit failures look to be an upstream Bazel bug; I filed issue #20599 for them. |
|
cc @toddmgreer @jmarantz @penguingao @mpwarres @capoferro as codeowners |
| } | ||
|
|
||
| SimpleHttpCache simple_cache_; | ||
| std::shared_ptr<StrictMock<MockHttpCache>> cache_; |
There was a problem hiding this comment.
Please document why this test needs the additional complexity of using a mock and having to respecify its behavior.
There was a problem hiding this comment.
Added a test fixture comment. This is most important for flow control tests in a future change - I can imagine splitting the test fixture to let the tests that don't need as fine-grained control use a fake, but for now this is at least an improvement over specifying the fake's behavior via extra copies of CacheFilter.
There was a problem hiding this comment.
It would make sense to me to add such a test in this PR to demonstrate the value, if that seems feasible.
I'm always skeptical with complex mocks whether they are mocking the right behavior, and wonder why (without looking too deeply) you think that's an improvement over using the CacheFilter itself.
There was a problem hiding this comment.
It's hard to add those tests before the CacheFilter implements its flow control logic: right now we can't test e.g. downstream backpressure while serving from cache because the whole response gets buffered, so there aren't any signals from the connection manager that we can interleave with cache body fragments.
I take your point about it being hard to trust that complex mocks are doing the right thing. My thinking is that when it comes to setting up the environment and reading results, it's even worse to trust that the code under test is doing the right thing. So in the existing state of the test, we test CacheFilter inserts by using CacheFilter lookups to verify the state of the cache, and we test lookups by using inserts to set up the cache, but we can never verify the behavior of one of those operations independently of the other. So there's a class of bugs that affect both insert and lookup that the existing test setup can never catch.
There was a problem hiding this comment.
Testing inserts and lookups independently is not a goal, because they have practically no independently defined behavior. The (simplified) spec is that if you insert something then look it up, you get the response you inserted (with certain modifications). We don't use CacheFilter lookups to verify the state of the cache--we use CacheFilter lookups to verify that the observable behavior of a particular sequence of operations is correct. If some "bug" doesn't change the observable behavior, then it isn't really a bug.
There are times when it is helpful to write tests that are less faithful to deployed reality, and testing a slow HttpCache is probably such a time. However, this PR changes all of the tests in this file to using mocks, including those that are better off using the real thing. Perhaps CacheFilterTest should have RealCacheFilterTest and MockCacheFilterTest subclasses, or should use a simple mock that defaults to letting SimpleHttpCache handle everything, with ON_CALLs only appearing in specific tests?
There was a problem hiding this comment.
In the past I've done some amount of insert/lookup testing against stats held in the cache object. That allows some modest testing of insert/lookup in isolation. E.g. you could verify that inserts fill the cache with data without doing a lookup. You could verify that lookups fail due to expiry. This doesn't require mocks.
@mbehr maybe a spec of what your testing plan is would make it easier to understand the future value of the mocks.
|
|
||
| // Headers of the cached response. | ||
| Http::ResponseHeaderMapPtr headers_; | ||
| Http::ResponseHeaderMapPtr headers_ = nullptr; |
There was a problem hiding this comment.
I think the preferred way in Envoy is to rely on default construction. (Please correct me if I'm mistaken.)
There was a problem hiding this comment.
That way doesn't compile for me; breaks on -Wmissing-field-initializers.
There was a problem hiding this comment.
did this get resolved somehow? Why is it compiling without initializers without your PR? What changed?
There was a problem hiding this comment.
Nothing ever constructed a LookupResult before this, just declared it with everything uninitialized and then set its fields. Is there a problem with doing it this way? We could explicitly define a constructor instead.
There was a problem hiding this comment.
I don't understand why it doesn't work to just revert all the initializers in this class and let them default-construct. You mentioned that doesn't compile for you which seems strange because it works in main today.
There was a problem hiding this comment.
What doesn't compile is the new constructor calls to LookupResult that I added in CacheFilterTest. They can't default-construct without these changes because -Wmissing-field-initializers prevents that. They weren't default-constructing before because the existing usage initialized them to indeterminate values.
There was a problem hiding this comment.
Yeah I bet if you add std::move you have to have a lot more detailed constructors. What's the implication if you drop that?
Other than that, after a quick scan, I just noticed a bunch of new code where you pass LookupResult by value and it might work better to pass by const ref.
Signed-off-by: Michael Behr <mkbehr@google.com>
…-filter-test-mock-cache Signed-off-by: Michael Behr <mkbehr@google.com>
|
Definitely breakage in this patch, not main; |
|
|
/wait |
Signed-off-by: Michael Behr <mkbehr@google.com>
|
Thanks for catching that - looks like #20599 was masking that breakage. Should be fixed now. |
|
Can we have @toddmgreer or @capoferro take a look first and then I'l do a final pass? |
jmarantz
left a comment
There was a problem hiding this comment.
lgtm mostly - the largest problem is I haven't been sold on the end-goal of these mocks being better than the current state, but I'm OK with it.
|
|
||
| // Headers of the cached response. | ||
| Http::ResponseHeaderMapPtr headers_; | ||
| Http::ResponseHeaderMapPtr headers_ = nullptr; |
There was a problem hiding this comment.
did this get resolved somehow? Why is it compiling without initializers without your PR? What changed?
| filter->setDecoderFilterCallbacks(decoder_callbacks_); | ||
| filter->setEncoderFilterCallbacks(encoder_callbacks_); | ||
| return filter; | ||
| constexpr absl::string_view kBody = "abc"; |
There was a problem hiding this comment.
envoy style convention for constants would use Body here but I'd suggest a slightly longer name for a file-scoped constant. Maybe CacheResponseBody?
| void SetUp() override { | ||
| ON_CALL(decoder_callbacks_, dispatcher()).WillByDefault(::testing::ReturnRef(*dispatcher_)); | ||
| cache_ = std::make_shared<StrictMock<MockHttpCache>>(); | ||
| filter_ = makeFilter(); |
There was a problem hiding this comment.
this was pre-existing, but why not just put all this in the CacheFilterTest ctor? Ditto for TearDown vs dtor.
| void expectCacheHitNoBody() { | ||
| // The filter should ask the cache for a lookup context, which will return a | ||
| // headers-only hit. | ||
| expectLookup({/*cache_entry_status_=*/CacheEntryStatus::Ok, |
There was a problem hiding this comment.
The /*cache_entry_status_=*/ annotation seems unnecessary given the well-qualified enum, here & below.
| std::make_unique<Envoy::Http::TestResponseHeaderMapImpl>(response_headers); | ||
| response_headers_from_cache->setCopy(Http::CustomHeaders::get().Age, age); | ||
| cb({/*cache_entry_status_=*/cache_entry_status, | ||
| /*headers_=*/std::move(response_headers_from_cache), |
There was a problem hiding this comment.
all these comments seem unnecessary in this case and just clutter the code.
We don't generally have these in Envoy style though there were cases above where you were explaining what the nullptr was for, and that case makes sense to me.
There was a problem hiding this comment.
sg, removed all of these that could be inferred from context.
| } | ||
|
|
||
| SimpleHttpCache simple_cache_; | ||
| std::shared_ptr<StrictMock<MockHttpCache>> cache_; |
There was a problem hiding this comment.
It would make sense to me to add such a test in this PR to demonstrate the value, if that seems feasible.
I'm always skeptical with complex mocks whether they are mocking the right behavior, and wonder why (without looking too deeply) you think that's an improvement over using the CacheFilter itself.
| filter->onDestroy(); | ||
| } | ||
| // Add Etag & Last-Modified headers to the response for validation | ||
| response_headers_.setReferenceKey(Envoy::Http::CustomHeaders::get().Etag, etag); |
There was a problem hiding this comment.
optional: this all might be easier to read (and slightly faster) if you put Envoy::Http::CustomHeaders::get() into a temp reference.
|
/wait |
Signed-off-by: Michael Behr <mkbehr@google.com>
| encodeHeaders_(testing::AllOf(IsSupersetOfHeaders(response_headers_), | ||
| HeaderHasValueRef(Http::CustomHeaders::get().Age, age)), | ||
| true)); | ||
| void expectLookup(LookupResult lookup_result, |
| std::function<void(MockLookupContext&)> context_callback = nullptr) { | ||
| // Wrap the move-only LookupResult in a copyable shared_ptr to fit with | ||
| // EXPECT_CALL(...).WillOnce()'s interface. | ||
| auto result_wrapper = std::make_shared<LookupResult>(std::move(lookup_result)); |
There was a problem hiding this comment.
will everything be cleaner if you just drop the std::move here?
| return lookup_context; | ||
| } | ||
|
|
||
| std::unique_ptr<MockLookupContext> makeLookupContext(LookupResult result) { |
| // Returns a std::function that can be used as an action with | ||
| // MockLookupContext::getHeaders to find response_headers_, with other fields | ||
| // of the LookupResult as specified. | ||
| std::function<void(LookupHeadersCallback)> getHeaders(LookupResult lookup_result) { |
|
|
||
| // Headers of the cached response. | ||
| Http::ResponseHeaderMapPtr headers_; | ||
| Http::ResponseHeaderMapPtr headers_ = nullptr; |
There was a problem hiding this comment.
Yeah I bet if you add std::move you have to have a lot more detailed constructors. What's the implication if you drop that?
Other than that, after a quick scan, I just noticed a bunch of new code where you pass LookupResult by value and it might work better to pass by const ref.
|
/wait |
|
This pull request has been automatically marked as stale because it has not had activity in the last 30 days. It will be closed in 7 days if no further activity occurs. Please feel free to give a status update now, ping for review, or re-open when it's ready. Thank you for your contributions! |
|
This pull request has been automatically closed because it has not had activity in the last 37 days. Please feel free to give a status update now, ping for review, or re-open when it's ready. Thank you for your contributions! |
Commit Message: Refactor CacheFilterTest to use a mock cache.
Additional Description: Add a mock cache and associated objects, and restructure the CacheFilter unit test to use them instead of SimpleHttpCache. This focuses the test on specifically exercising the CacheFilter's interface, and lets tests skip the extra requests they were sending to set up the cache for a lookup or verify the results of an insert. It'll also let future tests exercise more fine-grained control over the cache's behavior, e.g. to test flow control.
Risk Level: Low
Testing: bazel test //test/extensions/filters/http/cache/...
Docs Changes: N/A
Release Notes: N/A
Platform Specific Features: N/A