Technology Blogs by SAP
Learn how to extend and personalize SAP applications. Follow the SAP technology blog for insights into SAP BTP, ABAP, SAP Analytics Cloud, SAP HANA, and more.
cancel
Showing results for 
Search instead for 
Did you mean: 
matt_steiner
Active Contributor

"There are two hard things in computer science: cache invalidation, naming things, and off-by-one errors."

- Derivation of a quote from Phil Karlton


I thought it may be a good idea to kick off today's chapter with a joke as we'll cover a topic that many developers still see as an unpleasant necessity and a rather dull and boring topic: unit tests. It would be a wild shot to think I can change that perception, but if I can convince some of you to embrace unit testing right from the beginning of your next development project than I'm happy.

The way I see it unit tests are simply part of the job and if you take the time to develop the habit it helps you in coding more efficiently and - yes - faster, not to mention that it adds to better code quality!  In the previous chapters we have completely refactored the domain/persistence model and given that this is the foundation of our application it sounds like a good idea to make sure it works as expected. Or, in other words:

"If a program feature lacks an automated test, we assume it doesn’t work."

- from JUnit: A Cook's Tour


As I'll point out, the Spring framework used in the Enterprise Granny application comes with great support for JUnit and in most cases it will be quicker to test your code by running it as a JUnit test than deploying it to your server and testing it there. As such, I often develop new functionality in parallel to setting up the corresponding unit test - simply to speed up the process. Let's have a closer look what needs to be done to setup a unit test with Spring...

JUnit Tests in Spring

I'll only explain the most basic things that need to be done, so if you want more information please consult the corresponding documentation. In a nutshell, the basic layout of a JUnit test looks as outlined below:

package com.example.foo;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/**
* Tests for {@link Foo}.
*/
@RunWith(JUnit4.class)
public class FooTest
{
    @Test
    public void testSomething()
    {
          // ...
    }
}

In a Spring-based application there are only a few changes to this simple layout:

  • instead of using the regular JUnit4 runner class a special SpringJUnit4ClassRunner is used so that Spring can do the necessary bootstrapping (e.g. loading the application context) and provide the Dependency Injection features (for more details please refer to this documentation.)
  • we need to provide a reference to our application context configuration via the @ContextConfiguration annotation at class level (more details here)

That's it! With that we have a template to execute unit tests and benefit from all the Spring framework features. Let's have a look at the (skeleton of the) unit test we created for the ContactDAO class:

package com.sap.hana.cloud.samples.granny.dao;
// import statements
/**
* Tests for the {@link ContactDAO} class. 
*/
@ContextConfiguration("classpath:/META-INF/spring/app-context.xml")
@RunWith(SpringJUnit4ClassRunner.class)
@ActiveProfiles(profiles = "test")
public class TestContactDAO
{
          @Autowired
          ContactDAO contactDAO = null;
          /**
           * Tests the CRUD operations.
           */
          @Test
          @Transactional
          public void testCRUD()
          {
                    // ...
          }
          /**
           * Tests concurrent modification of an object.
           */
         @Test
          public void testOptimisticLocking()
          {
                  // ...
          }
          /**
           * Tests the query for contacts based on a given country.
           */
          @Test
          @Transactional
          public void testfindByAddressCountryQuery()
          {
                    // ...
          }
          /**
           * Creates a demo test {@link Contact}.
           *
           * @return A test {@link Contact}
           */
          static Contact createTestContact()
          {
                    // ...
          }
}

(The complete source can be found here.)

Let's discuss a couple of note-worthy things in the following sub-chapters...

ContextConfiguration (Line 😎

Via the @ContextConfiguration annotation we specify the location of our Spring application context configuration. In this scenario we'll simply point it to the regular app-context.xml file (which is the same we specified in the web.xml). This way, our unit test will use the exact same configuration as the application itself.  In some scenarios it may be necessary/preferable to provide a dedicated application configuration for unit tests (e.g. different database etc), and hence this feature comes in handy. I have explicitly opted to use the regular configuration and work around the issue of different environments by other means, which are explained next.

ActiveProfiles (Line 10)

One of the most recent features of Spring is the so-called environment abstraction:

"One of our most frequent requests has been to provide a mechanism in the core container that allows for registration of different beans in different environments. The word "environment" can mean different things to different users, but a typical scenario might be registering monitoring infrastructure only when deploying an application into a performance environment, or registering customized implementations of beans for customer A vs. customer B deployments. Perhaps one of the most common cases would be working against a standalone datasource in development vs looking up that same datasource from JNDI when in QA or production. Bean definition profiles represent a general-purpose way to satisfy use cases of this kind, and we'll explore the latter use case in the examples below."

- Chris Beams, Spring framework blog

I have used this approach to define two different DataSources: one for our web application (using the DataSource obtained from JNDI as declared in the web.xml) and one for our unit test (which starts up an in-memory Derby instance.) Take a look at the bottom of the app-context.xml file to see how the two profiles are set-up:

<beans profile="dev, prod" >
    <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/DefaultDB" />
</beans>
<beans profile="test">
    <bean id="dataSource" destroy-method="close" class="org.apache.commons.dbcp.BasicDataSource">
        <property name="driverClassName" value="org.apache.derby.jdbc.EmbeddedDriver"/>
        <property name="url" value="jdbc:derby:memory:DemoDB;create=true"/>
        <property name="username" value="demo"/>
        <property name="password" value="demo"/>
    </bean>
</beans>

Consequently, I have declared that the unit test should be run using "test" as the active profile. In the web.xml I have defined that the "prod" profile should be used:

<context-param>
  <param-name>spring.profiles.active</param-name>
  <param-value>prod</param-value>
</context-param>

Ultimately, we want our application to be environment-aware. For that purpose, we could implement a programmatic approach of defining the active profile(s) by implementing the ApplicationContextInitializer interface and wiring it up in the web.xml as follows:

<context-param>
  <param-name>contextInitializerClasses</param-name>
  <param-value>...</param-value>
</context-param>

We'll touch that topic agin in a later chapter and then I'll also show you how-to obtain some environment information about your SAP HANA Cloud Platform account.

Dependency Injection (Lines 13,14)

As you can see we can make use of the general DI features of Spring. Here, we use the @Autowired annotation to instruct Spring to inject the implementation of the ContactDAO interface during runtime.

CRUD Test (Lines 19-21)

Here, we have defined our test case for the CRUD operations. In order to flag it as a JUnit test we annotated it with @test. Please also note the @Transactional annotation, which results in the automatic creation of a transaction for the given method. While that may not be necessary within the context of a unit test running in an in-memory database (which is only alive during the execution of the unit test) I wanted to demonstrate best-practices here.

OptimisticLocking Test (Lines 29, 30)

This unit test verifies that the OptimisticLocking approach we implemented in chapter 5. To mimic a situation of a concurrent access I have omitted the @Transactional annotation on purpose.

Wrap-up

That's pretty much all there is to know to get started with unit tests. Maybe one last remark, following the Maven conventions the unit test should reside in the "src/test/java" source code folder to clearly separate the unit tests from the rest of the application (residing in "src/main/java").

Granny will take a well-deserved break now, but don't worry, we'll be right back in a few weeks and then we'll really kick it up a notch and do some more interesting things again. Here's an outlook of what'll be up to next:

  • implement a RESTful API (including some discussions about hypermedia-driven APIs including HATEOAS and HAL)
  • implement validation based on JSR-303 and introduce proper (error) message handling
  • implement a nice looking UI
  • ...

I hope to see you all back after the break! In the meanwhile, have a great summer and ... happy coding! For those that may want to take a sneak peak of what's coming, you can check out this branch - please note that it's a work in progress though!

PS: I got some feedback stating that the web application wasn't running anymore since our refactoring of the domain/entity model. True! At this point the application is in rather bad shape and a lot of the fundamentals are still not in place. As such it may not be a great idea to publish the application on the web as-is! Anyway, and while hurts to admit after all this writing about doing "things properly" I have come up with a "wild hack" that makes the original web application work somewhat with the new data model. Use it if you must, but keep in mind this is just a temporary hack that may work for the straightforward case, but it is certainly not a good example!

Or in short: do as I say, not as I do! :wink: