spring_boot_starter_test 0.4.0
spring_boot_starter_test: ^0.4.0 copied to clipboard
Testing for spring_boot_dart apps and starters: springBootTest, bean overrides for mocks, ApplicationContextRunner, matchers and log capture. Works with flutter_test.
spring_boot_starter_test #
No backward compatibility before 1.0. Any release may rename or change APIs, properties and behaviour; the breaking changes are listed in each package's CHANGELOG.
Testing support for spring_boot_dart applications
and starters, like spring-boot-starter-test. Built on test_api and
matcher (not package:test itself), so it works with both package:test
and flutter_test.
dev_dependencies:
spring_boot_starter_test: ^0.4.0
test: ^1.25.0 # or flutter_test
springBootTest: the whole application (@SpringBootTest) #
import 'package:spring_boot_starter_test/spring_boot_starter_test.dart';
import 'package:test/test.dart';
import '../bin/shop.spring.dart'; // generated springApplication
void main() {
final gateway = FakeGateway();
final app = springBootTest(
springApplication,
args: ['--shop.name=Night Books'],
properties: {'logging.console.enabled': false}, // @SpringBootTest(properties)
profiles: ['test'], // @ActiveProfiles
systemEnvironment: {'SHOP_MAX_BASKET_SIZE': '25'},
overrides: [BeanOverride<PaymentGateway>(gateway)], // @MockitoBean
);
test('pays through the fake', () {
app.getBean<Checkout>().pay(10);
expect(gateway.charged, [10]);
});
test('changes shared state', () {
app.getBean<Basket>().add(book);
app.markDirty(); // @DirtiesContext: the next test gets a fresh context
});
}
- Test config lives in
test/resources/(see below). - The context starts once before the first test of the
main/group(config files, auto-configuration, runners: all ofrunApplication) and closes withcloseAsync()after the last one. propertieswin over every other source, the command line included.dynamicProperties(@DynamicPropertySource) adds properties whose values are known only when the context starts (see below).systemEnvironmentis empty by default, so tests do not depend on the machine.SpringLoglevels and handler are restored afterwards.app.outputholds what the application printed (a failure report; the startup info goes throughSpringLog); it is also printed when a test fails.
test/resources/: config and data for tests only #
Like src/test/resources in Spring, test/resources/ comes first on the
classpath while springBootTest runs, and is never embedded into the
application (the generator embeds only lib/resources/):
lib/resources/application.properties # the application's config
test/resources/application-test.properties # test-only settings
test/resources/fixtures/books.json # classpath:fixtures/books.json in tests
final app = springBootTest(
springApplication,
profiles: ['test'], // + application-test.properties
propertyFiles: ['classpath:payments-test.properties'], // @TestPropertySource(locations)
);
- Prefer
application-test.propertieswithprofiles: ['test']: its keys are added on top ofapplication.properties. A file with the same path as alib/resources/file replaces it entirely (as in Spring), so atest/resources/application.propertieshides the real one. profilesreplacesspring.profiles.active(like@ActiveProfiles): list the application's profiles too if you need them (profiles: ['dev', 'test']).- Precedence:
properties>propertyFiles(later files win) > command lineargs> environment > config files. - Relative directories are resolved against the package of the test file
(the nearest
pubspec.yaml), so it does not matter whether the tests run from the package, a workspace root or an IDE. - Other directories:
resourceDirectories: ['test/fixtures', 'test/resources'](the first listed comes first). In browser tests there is no file system, so the directories are simply not found.
Dynamic properties (@DynamicPropertySource) #
Values known only when the tests run (a server's port, a temporary
directory) go through dynamicProperties. It is called whenever the
context starts (again after markDirty), and each supplier whenever the
property is read; these properties win over properties:
late HttpServer server;
setUpAll(() async => server = await HttpServer.bind('localhost', 0));
final app = springBootTest(
springApplication,
dynamicProperties: (registry) => registry
..add('catalog.url', () => 'http://localhost:${server.port}'),
);
Declare the setUpAll that provides the value before springBootTest, so
it runs first.
Web tests: MockMvc, webEnvironment #
With spring_boot_starter_web,
app.mockMvc calls the controllers in memory, like Spring's MockMvc:
the same dispatcher a real server runs (filters, mapping, binding, error
handling), no socket. By default (WebEnvironment.mock) a test starts no
server at all.
final app = springBootTest(springApplication);
test('finds a book', () async {
final mvc = app.mockMvc;
await mvc.get('/api/books/1111').andExpect(
status: 200,
contentType: MediaType.applicationJson,
headers: {'X-Trace': 'on'},
json: {'isbn': '1111', 'title': 'Dune'}, // or a matcher: containsPair('title', 'Dune')
);
await mvc.get('/api/books', params: {'q': 'dune'}).andExpect(json: hasLength(1));
await mvc
.post('/api/books', json: {'isbn': '', 'title': ''})
.andExpect(status: 400, contentType: MediaType.applicationProblemJson);
await mvc.delete('/api/books/2222').andExpect(status: 204);
});
get(path, headers:, params:),post/put/patch(path, json:, body:, headers:),delete(path, headers:), andperform(MockRequestBuilder(...))for anything else (MockRequestBuilder(HttpMethod.get, '/api/books/{isbn}', ['978-0'])withheaders,params,cookies,principal,content,json).andExpect(status:, contentType:, headers:, body:, json:)(bodyandjsontake a value or a matcher; a failure names the request and shows the response),andDo((result) => ...),andReturn()(anMvcResultwithrequestandresponse).ResultActionsis aFuture, soawaitit.- Server-sent events: the result is ready as soon as the headers are
out; read the events as they come from
result.response.eventStream(eventsfor those so far) and end the stream withresult.cancel(). - Uploads:
mvc.multipart(path, files: [...], params: {...})sends amultipart/form-datarequest;MockMultipartFile('cover', 'dune.png', 'image/png', bytes)is a file, one without a file name and withapplication/jsona JSON part for@RequestPart. Downloads are read fromresult.response.bodyBytes(withRange/If-Modified-Sinceheaders as needed). - WebSocket endpoints (
spring_boot_starter_websocket):await mvc.webSocket('/ws/chat')opens a connection in memory and returns the client'sWebSocketSession. RequestPostProcessorchanges a request before it is performed (Spring's.with(...); Spring Security'suser(...)andcsrf()are ones):builder.with_(processor), or for every requestMockMvc(app.getBean<DispatcherHandler>(), defaultPostProcessors: [...]).
A real server on a free port, for clients that need a socket (a Feign
client, dart:io's HttpClient, a WebSocket client):
final app = springBootTest(springApplication, webEnvironment: WebEnvironment.randomPort);
test('over HTTP', () async {
final url = 'http://localhost:${app.localServerPort}/api/books/1111';
...
});
webEnvironment |
Spring | Server |
|---|---|---|
WebEnvironment.mock (default) |
MOCK |
none; app.mockMvc |
WebEnvironment.randomPort |
RANDOM_PORT |
a real one on a free port: app.localServerPort (local.server.port); the context starts once it listens |
WebEnvironment.definedPort |
DEFINED_PORT |
a real one on server.port |
WebEnvironment.none |
NONE |
none |
As a test class: @SpringBootTest(webEnvironment: WebEnvironment.randomPort),
@LocalServerPort() late int port;, and @Autowired() late MockMvc mvc;
(@AutoConfigureMockMvc() on the class states it, as in Spring; it is
not required).
ApplicationContextRunner starts no server either (it sets
spring.test.web-environment=mock); add
withPropertyValues(['spring.test.web-environment=random-port']) for a
real one.
BeanOverride: fakes and mocks (@MockitoBean, @TestBean) #
BeanOverride<PaymentGateway>(FakeGateway()) // the only bean of the type
BeanOverride<PaymentGateway>(MockGateway(), name: 'sandbox') // one of several
The original definition is removed before any bean is created, so the real
object is never built. The replacement keeps the name, aliases, qualifiers
and @Primary / @Fallback, so @Qualifier injection points still get it.
Use any fake or mock library (e.g. mocktail). Overriding a type with
several beans without name: replaces the @Primary one, like Spring;
without a single primary bean it fails and lists them. name: may also be
an alias of the bean.
ApplicationContextRunner: testing auto-configuration #
The tool for starter authors (Spring Boot's ApplicationContextRunner).
Immutable; no config files; empty environment unless set.
final runner = const ApplicationContextRunner()
.withAutoConfiguration(neonCacheAutoConfiguration);
test('default cache', () {
runner.run((context) {
expect(context, hasSingleBean<Cache>());
});
});
test('backs off for the application cache', () {
runner.withBean<Cache>(AppCache.new).run((context) {
expect(context, hasSingleBean<Cache>()); // a missing back-off: two beans
expect(context.getBean<Cache>(), isA<AppCache>());
});
});
test('needs a size', () {
runner.withPropertyValues(['neon.cache.size=-1']).run((context) {
expect(context, hasFailed(isA<BeanCreationException>()));
});
});
withBean<T>(supplier, name: ...)registers an extra bean before the configurations are processed, like Boot'swithBean(type, supplier)(each run gets a new instance fromsupplier): a@ConditionalOnMissingBeanbacks off, and a missing back-off shows up as a second bean (or, with the same name, a failed start). It never replaces a bean.withBeanOverride<T>(instance, name: ...)replaces a bean after the configurations were processed, like aBeanOverride(not in Boot's runner; preferwithBeanto test back-offs).withPropertyValues(['a=b', 'c: d', 'flag'])takeskey=valueandkey:valuelike Boot'sTestPropertyValues(trimmed; no separator: an empty value).- Also
withUserConfiguration(module)(registered before the auto-configuration),withSystemEnvironment,withInitializer.
For asynchronous checks use runAsync: the context stays open until the
returned future completes and is then closed with closeAsync() (run
closes it with close() as soon as the body returns).
test('calls the client', () async {
await runner.runAsync((context) async {
expect(await context.getBean<CatalogClient>().find('1'), isNotNull);
});
});
Matchers #
hasSingleBean<T>(), hasBean<T>(), doesNotHaveBean<T>(),
hasBeanNamed('name'), hasFailed([matcher]). They accept an
AssertableApplicationContext, a springBootTest context or any
ListableBeanFactory, and say which beans were found when they fail. Like
AssertJ's, the bean-count matchers include the parent contexts
(withParent).
captureLogs #
final logs = captureLogs(level: SpringLogLevel.debug);
test('warns about slow payments', () {
checkout.pay(slowOrder);
expect(logs.messages, contains('slow payment'));
expect(logs.atLevel(SpringLogLevel.warning), hasLength(1));
expect(logs.ofLogger('neon.checkout'), isNotEmpty);
});
Every test starts with an empty capture.
Differences from Spring #
springBootTestis a function for the tests of amainorgroup(test classes with@SpringBootTestare generated into it); no test slices (@WebMvcTest,@DataJpaTest, ...) and no@TestConfiguration: pass extra modules to theSpringApplication.webEnvironmentdefaults tomockas in Spring, butMockMvcneeds no@AutoConfigureMockMvc:app.mockMvc(or an@Autowired MockMvcfield) always works with the web starter.andExpecttakes named expectations (status:,json:with matchers) instead ofstatus().isOk()andjsonPath(...); there is noWebTestClientorTestRestTemplate(use a Feign client orHttpClientagainstlocalServerPort).BeanOverridereplaces the bean definition with a ready-made object of your own (like@TestBean); there are no generated mocks or spies and no automatic reset between tests (@MockitoBean(reset)): use any mock library and reset insetUp. Without an existing bean, the instance is registered.ApplicationContextRunner.withBeantakes a supplier and the type as a type argument (withBean<Cache>(AppCache.new)), not a class; there is no constructor-arguments variant.withBeanOverridehas no Boot counterpart.ApplicationContextRunner.withAutoConfiguration(module, metadata)takes the module's@AutoConfigurationModuleby hand (Dart cannot read the annotation of a variable at runtime); the modules are then sorted likeAutoConfigurations.of(...)(name,order,before/after).withAllowCircularReferences(true)fails every run with anUnsupportedError: circular references are always an error here (no early references);falseis the default, as in Boot.- No context caching across test files: each
springBootTeststarts its own context (shared by the tests of itsmain/group). dynamicPropertiesis a parameter, not an annotated static method; noDynamicPropertyRegistrarbeans.captureLogsreplacesOutputCaptureExtensionforSpringLogoutput.
Name clash #
package:test (via matcher) and spring_core_dart both define
Description (Spring's @Description). It only matters if a test uses it:
import 'package:test/test.dart' hide Description;.