Migrate old wiki component tests best practices 21/94821/1
authorGuillaume Lambert <guillaume.lambert@orange.com>
Tue, 26 Jan 2021 15:19:46 +0000 (16:19 +0100)
committerGuillaume Lambert <guillaume.lambert@orange.com>
Tue, 26 Jan 2021 15:19:46 +0000 (16:19 +0100)
to developper guides and rework them
https://wiki-archive.opendaylight.org/view/BestPractices/Component_Tests

JIRA: TSC-296
Signed-off-by: Guillaume Lambert <guillaume.lambert@orange.com>
Change-Id: I5ea66eae18e4271ea5d010ff8d641daebd05db8a

docs/conf.py
docs/developer-guides/tests/component.rst [new file with mode: 0644]
docs/developer-guides/tests/considerations.rst
docs/developer-guides/tests/index.rst

index 15713e0657c9b91064979c1c46ae1980711e972d..6ffd5b063ac78ebe6b8c05cf73e3b539e807d574 100755 (executable)
@@ -63,6 +63,8 @@ linkcheck_ignore = [
     '^http[s]://github.com/.*#',
     # Ignore webchat anchors because '#' is not an anchor for this app
     'https://webchat.freenode.net/#',
+    # Ignore google docs anchors
+    'https://docs.google.com/presentation/d/1bnwj8CrFGo5KekONYSeIHySdkoXZiewJxkHcZjXnzkQ/edit#slide=id.g17d8ae4d92_0_137',
 ]
 linkcheck_timeout = 300
 
diff --git a/docs/developer-guides/tests/component.rst b/docs/developer-guides/tests/component.rst
new file mode 100644 (file)
index 0000000..e9e9f7a
--- /dev/null
@@ -0,0 +1,321 @@
+.. _component:
+
+############################
+Component Tests (with Guice)
+############################
+
+*Note: This document is a work in progress.*
+
+Introduction
+============
+
+Code which uses `dependency injection with standard
+annotations <https://wiki-archive.opendaylight.org/view/BestPractices/DI_Guidelines>`__
+is well suite for component tests.
+
+Component tests cover the functionality of 1 single ODL bundle (e.g.
+netvirt's aclservice, genius' interfacemanager, etc.) They are focused
+on end-to-end testing of APIs, not individual internal implementation
+classes (that's what Unit Tests are for). They focus on testing the code
+in their own module, and typically stub required external services. They
+assert outcome on the system, often those external services, and the
+data store. They wire together the internal beans through a Dependency
+Injection (DI) Framework called Guice, which leverages standard Java
+annotations in the code, which is equally used by Blueprints.
+
+This `presentation from the ODL Summit
+2016 <https://docs.google.com/presentation/d/1bnwj8CrFGo5KekONYSeIHySdkoXZiewJxkHcZjXnzkQ/edit#slide=id.g17d8ae4d92_0_137>`__
+has some content related to this topic.
+
+Maven
+=====
+
+In order to use Google Guice in your end2end API component tests to wire
+bean objects using the same annotations as BP, use:
+
+.. code:: xml
+
+   <dependency>
+     <groupId>org.opendaylight.infrautils</groupId>
+     <artifactId>inject.guice.testutils</artifactId>
+     <version>${infrautils.version}</version>
+     <scope>test</scope>
+   </dependency>
+
+Code
+====
+
+Object Wiring Binding
+---------------------
+
+You can write Guice object binding wiring classes like this:
+
+.. code:: java
+
+   public class AclServiceModule extends AbstractGuiceJsr250Module {
+       @Override
+       protected void configureBindings() {
+           bind(AclServiceManager.class).to(AclServiceManagerImpl.class);
+           bind(AclInterfaceStateListener.class);
+       }
+       ...
+   }
+
+for any OSGi service external to the bundle (not local bean) you use
+bind() like this:
+
+.. code:: text
+
+   bind(DataTreeEventCallbackRegistrar).annotatedWith(OsgiService.class).to(DataTreeEventCallbackRegistrarImpl.class)
+
+JUnit with GuiceRule
+--------------------
+
+Use \*Module classes which define Object Wiring Binding in a JUnit \* Test class
+like this:
+
+.. code:: java
+
+   public @Rule MethodRule guice = new GuiceRule(AclServiceModule.class, AclServiceTestModule.class);
+
+   @Inject DataBroker dataBroker;
+   @Inject AclServiceManager mdsalApiManager;
+   @Inject AclInterfaceStateListener mdsalApiManager;
+
+Async
+=====
+
+In these Component Tests (more so than in simple Unit Tests), one often
+hits problems due to the extensive use of highly asynchronous code in
+ODL. Some progress has been made with testing utilities for each
+respective async API, detailed in this chapter.
+
+genius AsyncClusteredDataTreeChangeListenerBase & AsyncDataTreeChangeListenerBase
+----------------------------------------------------------------------------------
+
+In order to make a test wait for something which happens in a
+AsyncClusteredDataTreeChangeListenerBase or
+AsyncDataTreeChangeListenerBase subclass before then asserting on the
+outcome of what happened, you just add the
+TestableDataTreeChangeListenerModule to the GuiceRule of your \*Test,
+and then @Inject AsyncEventsWaiter asyncEventsWaiter, and use
+awaitEventsConsumption() AFTER having done action like a data store
+write for which a listener should kick in, and BEFORE reading the
+datastore to check the effect:
+
+.. code:: java
+
+   public final @Rule MethodRule guice = new GuiceRule(
+       new YourTestModule(),
+       new TestableDataTreeChangeListenerModule());
+   @Inject AsyncEventsWaiter asyncEventsWaiter;
+   asyncEventsWaiter.awaitEventsConsumption();
+
+If a AsyncClusteredDataTreeChangeListenerBase or
+AsyncDataTreeChangeListenerBase (subclass) has "fired", then the
+AsyncEventsWaiter verifies that a test has indeed used
+awaitEventsConsumption() - and fails the test with
+IllegalStateException: Test forgot an awaitEventsConsumption() if it
+does not. This mechanism ensures that a test does not "forget" to
+awaitEventsConsumption and assert an expected outcome. NB however that
+if the test runs fast, it may end before the listeners kicked in, and
+the IllegalStateException may not always been seen (i.e. leading to a
+"heisenbug", found with the RunUntilFailureRule). Therefore, if in your
+test you do not need to awaitEventsConsumption() at all, then you should
+not use the TestableDataTreeChangeListenerModule. However, this is
+likely an indication of lack of better test coverage in your test - you
+probably do want to assert on the effect of your
+AsyncClusteredDataTreeChangeListenerBase or
+AsyncDataTreeChangeListenerBase subclasses?
+
+infrautils JobCoordinator (formerly genius DataStoreJobCoordinator)
+---------------------------------------------------------------------
+
+similarly to above, using the JobCoordinatorEventsWaiter:
+
+.. code:: text
+
+   @Inject JobCoordinatorEventsWaiter coordinatorEventsWaiter;
+   coordinatorEventsWaiter.awaitEventsConsumption();
+   (TODO still need to be ported from genius to infrautils)
+   (TODO need to write a combined AsyncEventsWaiter instead of doing e.g. InterfaceManagerTestUtil's waitTillOperationCompletes)
+
+It is HIGHLY (!) recommended to FIRST switch code from the @Deprecated
+DataStoreJobCoordinator (in genius) to the JobCoordinator (in
+infrautils), because that does not suffer from the problem where a
+background job can "continue on" from one @Test method into another
+@Test, or even from one \*Test class into another, due to use of
+"static", which can lead to VERY confusing log messages.
+
+genius ResourceBatchingManager
+-------------------------------
+
+The ResourceBatchingManager API does not yet have an AsyncEventsWaiter
+companion.
+
+Other
+-----
+
+Some of our "new style" Component Tests, such as e.g.
+InterfaceManagerConfigurationTest, and others, still need Thread.sleep()
+in some places.. the eventual goal is to be able to eventually
+completely eliminate them from all tests.
+
+Tutorial
+========
+
+Let's imagine you want to make a change e.g. in aclservice, just as an
+example. Specifically, you've added a new argument for another new
+internal bean or external service to the @Singleton
+AclServiceManagerImpl @Inject annotated constructor, let's say to an
+IdManagerService for the sake of this example discussion.
+
+A component test based on Guice wiring, such as AclServiceTest, will now
+fail on you with a message saying something like this:
+
+*No implementation for (...your new service...) was bound while locating
+(...) for the X-th parameter of AclServiceManagerImpl.*
+
+The \*Module classes referenced from the GuiceRule in a \*Test is where
+the wiring is defined - that's what determines, for that test, what
+implementation class is bound to what service interface etc. If you have
+a look at e.g. the AclServiceModule & AclServiceTestModule, it should be
+obvious what that does - just 1 single line for each binding.
+
+The error message shown above simply means that an interface was
+encountered but you have not specified what implementation you would
+like to use for that interface in a given test. (Different tests could
+have different Module with varying bindings; but don't have to.)
+
+To fix this after having made your change, you would now have to add 1
+line in AclServiceTestModule to do a bind() of IdManagerService to...
+something.
+
+If IdManagerServiceas was some new internal helper class of aclservice
+which you would like to test, then you would just do:
+
+.. code:: java
+
+   bind(IdManagerService.class).to(YOURIdManagerServiceImpl.class);
+
+The YOURIdManagerServiceImpl would have a @Singleton annotation on its
+class, and have an @Inject annotation on its constructor, to
+automatically get its dependencies injected (and perhaps have
+@PostConstruct and @PreDestroy, if it has a "lifecycle"; or extend
+AbstractLifecycle). This is further documented on the `DI
+Guidelines <https://wiki-archive.opendaylight.org/view/BestPractices/DI_Guidelines>`__
+page.
+
+Now, in the case of an existing ODL service from another project, you
+typically didn't actually write your own implementation of the
+IdManagerService interface. At full system runtime, you probably would
+like that to use the IdManager class (and you probably added that to
+your BP XML). So, having understood above, you COULD now be tempted to
+add this in AclServiceTestModule:
+
+.. code:: java
+
+   bind(IdManagerService.class).to(IdManager.class);
+
+but there is two problems with this, 1. small practical (easy to fix),
+2. conceptual (more important):
+
+1. IdManager at the time of initially writing this documentation didnt't
+have @Singleton @Inject and @PreDestroy on its close() .. this may have
+already changed - or you could, easily, make a contribution to Genius to
+change that; I'd recommend making IdManager extend AbstractLifecycle in
+this case. This can theoretically, even though we wouldn't recommend
+that, also be worked-around by doing the IdManager "wiring" manually
+through 2 lines of like new IdManager(...) and then use
+bind(IdManagerService.class) .toInstance(myIdManager). BUT...
+
+2. ... it would, typically IMHO, be wrong to use IdManager as
+IdManagerService implementation in AclServiceTest. This is more of a
+general recommendation than a hard rule. The idea is that the component
+test of aclservice should NOT have to depend on the real implementation
+of all external services the aclservice code depends on (only all of the
+internal beans of aclservice). So it would, generally, be considered
+better to bind a local test implementation of IdManager, which does the
+minimum you need for the test. A full coverage test of IdManager would
+be the responsibility of genius' idmanager-impl, not aclservice-impl. So
+what I would probably start with doing in your case, unless there is a
+very strong need that you must absolutely have the "full" IdManager for
+the AclServiceTest, is to just put this into AclServiceTestModule's
+configure() method:
+
+.. code:: java
+
+   // import static org.opendaylight.yangtools.testutils.mockito.MoreAnswers.*;
+   bind(IdManagerService.class).toInstance(Mockito.mock(IdManagerService.class, exception());
+
+Doing this will resolve the Guice Exception you have run into below. But
+whenever some aclservice code now actually calls a method
+IdManagerService, you'll get an UnstubbedMethodException .. and this is
+normal - because you just mocked IdManagerService! I would still
+recommend to start like this, and then go about fixing
+UnstubbedMethodException as they arise when you run AclServiceTest ...
+
+Let's for example say that your new code calls IdManagerServices'
+allocateIdRange() method somewhere - I don't know if it does, so this is
+just for Illustration. You could make your mocked IdManagerService do
+something else than throw a UnstubbedMethodException for
+allocateIdRange() in two different "styles", this is somewhat dependant
+on personal preference:
+
+A) Write out a partial "fake" implementation of it:
+
+Write an inner class right there inside at the end of the
+AclServiceTestModule.java - just because it's easier to have this
+together and immediately evident when reading code; unless it becomes
+very long, in which case you could also move it outside, of course:
+
+.. code:: java
+
+   private abstract static class TestIdManagerService implements IdManagerService {
+       @Override
+       public Future<RpcResult<AllocateIdOutput>> allocateId(AllocateIdInput input) {
+       // TODO do something minimalistic here, just useful for the test, not a general implementation
+       }
+   }
+
+Note that the code in such test service implementations are typically
+simplistic and trivial, and not "real full fledged". Note also that only
+methods which the test actually requires are implemented; because it's
+abstract, we don't have to write anything at all for other methods of
+the interface.
+
+You can then change the binding in configure() to be:
+
+.. code:: text
+
+   bind(IdManagerService.class).toInstance(Mockito.mock(TestIdManagerService.class, realOrException())
+
+Note the subtle difference with the use of realOrException() instead of
+just exception().
+
+This first style is vorburger's personal preference; finding this code
+clearer to read and understand for anyone than "traditional" Mockito
+usage, and not minding to have to type a few extra lines (for the
+class), which the IDE will put for me on Ctrl-Space anyway, than having
+to understand the Mockito magic. This is particular true when the
+implemented methods have anything but non-trivial arguments and return
+types - which is often the case in ODL.
+
+B) Write the implementation using traditional Mockito API:
+
+Write a method, just for clarify, such as:
+
+.. code:: java
+
+   private IdManagerService idManagerService() {
+       IdManagerService idManagerService = Mockito.mock(IdManagerService.class);
+       Mockito.when(idManagerService.allocateId(...)).thenReturn(...);
+       // etc.
+       return idManagerService;
+   }
+
+and then changing the binding in configure() to be:
+
+.. code:: java
+
+   bind(IdManagerService.class).toInstance(idManagerService());
index 23d19bde171f9ba9f14b3bac485c161c200d17a3..03480ad80b8c7911e59cdf5084f4f652b7045b65 100644 (file)
@@ -130,8 +130,7 @@ Component Tests
 
 It's not that hard to write Component Tests which test the interaction
 of a number of interrelated services, without going to a full blown and
-much "heavier" Karaf OSGi integration test just yet; see `Component
-Testing <https://wiki-archive.opendaylight.org/view/BestPractices/Component_Tests>`__.
+much "heavier" Karaf OSGi integration test just yet; see :ref:`component`.
 
 Integration Tests
 =================
@@ -143,5 +142,5 @@ environment <https://wiki-archive.opendaylight.org/view/CrossProject:HouseKeepin
 Recommended Reading
 ===================
 
--  http://googletesting.blogspot.ch/2013/07/testing-on-toilet-know-your-test-doubles.html
--  http://martinfowler.com/articles/mocksArentStubs.html
+-  https://googletesting.blogspot.ch/2013/07/testing-on-toilet-know-your-test-doubles.html
+-  https://martinfowler.com/articles/mocksArentStubs.html
index 2a06193c0d9f8bbf5bcd35d2cd1c5bb236282bc9..815c7033baea631dedd3c75ff5e7c2c43eda1234 100644 (file)
@@ -11,3 +11,4 @@ Generic
 
    junit
    considerations
+   component