spring_boot_starter_test 0.2.2
spring_boot_starter_test: ^0.2.2 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.2.2
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.
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, not an annotation on a test class; nowebEnvironment(there is no embedded web server), no test slices (@WebMvcTest,@DataJpaTest, ...) and no@TestConfiguration: pass extra modules to theSpringApplication.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;.