DEV Community

Matt Raible for Okta

Posted on • Originally published at developer.okta.com on

The Hitchhiker's Guide to Testing Spring Boot APIs and Angular Components with WireMock, Jest, Protractor, and Travis CI

Writing apps with Spring Boot and Ionic (which is built on Angular) can be fun because both frameworks offer a good developer experience (DX). You change a file, save it, and the app automatically reloads with your changes. This feature is often called hot-reload and it's a blissful way to develop. It’s so nice, it can cause you to forget about automating your tests.

I'll admit, writing tests is difficult to do. Writing the test often isn't difficult, it's just code after all. However, writing a test first is a habit that's tricky to form. However, if you do test-driven development (TDD), chances are that you'll develop a nicer API. After all, your test will act as a client to your API, and you'll see what code looks like for your users.

A couple of months ago, I wrote posts about developing a cryptocurrency wealth tracking PWA and storing your holdings as Okta custom profile attributes. To show you how to write tests for the back-end and front-end components, I recruited Brian Demers to help me. Brian is the lead developer on Okta's Java SDKs and a fun guy to hang out with.

Brian wrote the sections of this article that pertain to the backend code (Java + Spring Boot), while I wrote the sections that pertain to the frontend (TypeScript + Angular). When you see “I” in each section, it’s either Brian or myself, depending on context.

In this tutorial, you'll see how to implement many aspects of testing a modern application:

I like to call this a hitchhiker's guide because you can jump into any section of this guide and learn how to test those particular components. If you have any suggestions for improving a specific part, please let us know so we can improve it! We try to keep our blog posts up-to-date as much as possible.

To begin, you'll want to clone the GitHub repository that contains the Ionic PWA and Spring Boot API and check out the testing-start branch.

git clone -b testing-start https://github.com/oktadeveloper/okta-ionic-crypto-java-sdk-example.git
Enter fullscreen mode Exit fullscreen mode

The instructions below assume you're operating from the okta-ionic-crypto-java-sdk-example directory of this cloned project.

Set Up Okta

First off, what is Okta? Simply put, we’re an API service that allows developers to create, edit, and securely store user accounts and user account data, and connect them with one or multiple applications. Our API enables you to:

In short: we make user account management a lot easier, more secure, and more scalable than what you’re probably used to.

To get started with this tutorial, you’ll first need to log into your forever-free Okta developer account,or sign up if you don't already have one.

Once you’re signed up and logged in, you’ll need to add a holdings attribute to your organization's user profiles. Log in to the Okta Developer Console, then navigate to Users > Profile Editor. Click on Profile for the first profile in the table. You can identify it by its Okta logo. Click Add Attribute and use the following values:

  • Display name: Holdings
  • Variable name: holdings
  • Description: Cryptocurrency Holdings

You will need to create an API Token and OIDC App to get values to perform authentication and store data in Okta.

Navigate to Applications > Add Application. Click Single-Page App, click Next, and give the app a name you'll remember. Click Done.

For the Okta Java SDK to talk to Okta's API, you'll need to create an API token. The abbreviated steps are as follows:

  1. Log in to your Developer Console
  2. Navigate to API > Tokens and click Create Token
  3. Give your token a name, then copy its value

Server Configuration for Your Spring Boot API

Open holdings-api/src/main/resources/application.properties and add your API token as a property. While you're there, set the issuer and clientId to match your OIDC application.

NOTE: The value of {yourOktaDomain} should be something like dev-123456.oktapreview. Make sure you don't include -admin in the value!

okta.oauth2.issuer=https://{yourOktaDomain}.com/oauth2/default
okta.oauth2.clientId={yourClientId}
okta.client.token=XXX
Enter fullscreen mode Exit fullscreen mode

NOTE: If you don't want these values specified in your source code, you can set them as environment variables. For example, OKTA_CLIENT_TOKEN is the name of the environment variable that will override the okta.client.token property. See Spring Boot's externalized configuration documentation to learn more.

Client Configuration for Your Ionic PWA

For the client, set the issuer and copy the clientId into crypto-pwa/src/pages/login/login.ts.

const config = {
  issuer: 'https://{yourOktaDomain}.com/oauth2/default',
  redirectUri: window.location.origin + '/implicit/callback',
  clientId: '{clientId}'
};
Enter fullscreen mode Exit fullscreen mode

Add Unit Tests for Your Spring Boot Controllers

Everyone has opinions on what unit testing is and what it means. To me, unit tests focus on executing the smallest block of code possible. In simple terms, exercising your code's public methods.

The tests in the following sections were added in a pull-request. In addition to adding tests, I also changed the Holding bean's setters to be fluent, so the methods can be chained together as you will see below.

Spring has a lot of helper test infrastructure to help set up the context of your application. When possible, I avoid using it, this allows for more focused and faster running tests. How you inject your dependencies into your code changes the option you have for testing. For example, if class we are testing HoldingsController was written with field injection it would look something like this:

public class HoldingsController {

    @Autowired
    private Client client;

    public HoldingsController() {}
    ...
}
Enter fullscreen mode Exit fullscreen mode

The above implementation can only be run inside a Spring container. Instead, the same code can be refactored to use constructor injection:

public class HoldingsController {
    private final Client client;

    public HoldingsController(Client client) {
        this.client = client;
    }
    ...
}
Enter fullscreen mode Exit fullscreen mode

The differences between the two might may or may not be obvious to you. The second example is just plain ol’ Java code: Client field is final, once the object is constructed is it ready to be used, and, to me most significantly, it can be used without a Spring. Constructor injection makes it much easier to write tests; you can test your code in isolation and mock out the behavior you are testing for. You can read on the topic from Oliver Gierke.

When given a choice, my preferred testing frameworks are Mockito and Hamcrest. Mockito makes it easy to stub behavior, and Hamcrest has both excellent default assertion messages, and it helps isolate your test code from the differences between JUnit and TestNG. I usually write tests in Groovy as well to help reduce some of the boilerplate, but I'll stick with Java for today.

To refresh your memory, I'm going to write tests for the HoldingsController; this class has a single constructor and methods for @GetMapping and @PostMapping. I'll focus on the saveHoldings(@RequestBody Holding[] holdings, Principal principal) method:

@PostMapping
public Holding[] saveHoldings(@RequestBody Holding[] holdings, Principal principal) {
    User user = client.getUser(principal.getName());
    try {
        String json = mapper.writeValueAsString(holdings);
        user.getProfile().put(HOLDINGS_ATTRIBUTE_NAME, json);
        user.update();
    } catch (JsonProcessingException e) {
        logger.error("Error saving Okta custom data: " + e.getMessage());
        e.printStackTrace();
    }
    return holdings;
}
Enter fullscreen mode Exit fullscreen mode

This method saves the argument holdings to an Okta custom profile property associated with the user.

Mock Okta's Java SDK with Mockito

Mockito and Hamcrest are both typically used with static imports, something like:

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.mockito.Mockito.*;
Enter fullscreen mode Exit fullscreen mode

For those of you who don't like wildcard imports:

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.is;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.eq;
Enter fullscreen mode Exit fullscreen mode

I've defined a saveHoldingsTest in HoldingsControllerTest in which I created a few mock objects:

Client client = mock(Client.class);
User user = mock(User.class);
UserProfile userProfile = mock(UserProfile.class);
Principal principal = mock(Principal.class);
Enter fullscreen mode Exit fullscreen mode

Then define the behaviour of the mocks:

String username = "joe.coder@example.com";
when(principal.getName()).thenReturn(username);
when(client.getUser(username)).thenReturn(user);
when(user.getProfile()).thenReturn(userProfile);
Enter fullscreen mode Exit fullscreen mode

You can see the behavior driven Given-When-Then style of Mockito (where, in this case, the 'given' is the mock definition). Anytime principal.getName() is called "joe.coder@example.com" will be returned and when client.getUser("joe.coder@example.com") is called, our mocked User instance is returned.

Now for the easy part, calling HoldingsController.saveHoldings()

Holding[] inputHoldings = new Holding[] {
        new Holding()
            .setCrypto("crypto1")
            .setCurrency("currency1")
            .setAmount("amount1"),
        new Holding()
            .setCrypto("crypto2")
            .setCurrency("currency2")
            .setAmount("amount2")
};

HoldingsController holdingsController = new HoldingsController(client);
Holding[] outputHoldings = holdingsController.saveHoldings(inputHoldings, principal);
Enter fullscreen mode Exit fullscreen mode

Nothing special here, but that is the point! Using constructor injection allows us to treat this object like any other Java object.

The only thing left is to validate the results. Using Hamcrest, we can verify the saveHoldings method returns a Holding[] equal to the input.

assertThat(outputHoldings, is(inputHoldings));
Enter fullscreen mode Exit fullscreen mode

We also need to validate that the custom property holdings was set, and user.update() was called, for this we will use a Mockito ArgumentCaptor to capture the JSON string value of holdings:

ArgumentCaptor<String> holdingsJsonCaptor = ArgumentCaptor.forClass(String.class);
verify(userProfile).put(eq("holdings"), holdingsJsonCaptor.capture());
verify(user).update();
Enter fullscreen mode Exit fullscreen mode

Finally, we can validate the JSON string. Spotify Hamcrest (yes, the same Spotify you rock out to while coding) is my new favorite testing lib and shows the power and readability of custom Hamcrest matchers.

JsonNode holdingsParsed = new ObjectMapper().readTree(holdingsJsonCaptor.getValue());
assertThat(holdingsParsed, jsonArray(contains(
        jsonObject()
            .where("crypto", jsonText("crypto1"))
            .where("currency", jsonText("currency1"))
            .where("amount", jsonText("amount1")),
        jsonObject()
            .where("crypto", jsonText("crypto2"))
            .where("currency", jsonText("currency2"))
            .where("amount", jsonText("amount2"))
)));
Enter fullscreen mode Exit fullscreen mode

I mentioned above Hamcrest has great default assertion messages, hopefully you will never have to see them, but that isn't likely, here is an example output of a failed JSON assertion:

java.lang.AssertionError:
Expected: an array node whose elements iterable containing [{
  "crypto": a text node with value that is "crypto1"
  "currency": a text node with value that is "currency1"
  "amount": a text node with value that is "amount1"
}, {
  "crypto": a text node with value that is "crypto2"
  "currency": a text node with value that is "currency2"
  "amount": a text node with value that is "amount2"
}]
     but: was an array node whose elements item 0: {
  "crypto": was a text node with value that was "wrong-crypto"
  ...
}
Enter fullscreen mode Exit fullscreen mode

Mock Okta's API with WireMock

Integration tests are a little more involved. Sometimes you spin up your full application and all of its required dependencies. Other times you mock out the external dependencies so you can focus on your code (and not worry about needing an internet connection just to run your tests). I'm a big fan of the later and try to take that approach when possible.

The HoldingsApiApplication has two external dependencies, the Okta OAuth 2.0 IdP and the Okta Management API. I'm going to use WireMock for both. For OAuth mocking, you also have another option, but it requires changes to how your application is running while testing. I prefer to make as few changes as possible to my application during integration testing. I'll give you a link to compare the two strategies at the end of this section.

As you start to look at HoldingsApiIT you will see a few interesting things:

@RunWith(SpringRunner.class)
@ContextConfiguration(initializers = HoldingsApiIT.RandomPortInitializer.class)
@SpringBootTest(classes = {HoldingsApiApplication.class},
                webEnvironment = RANDOM_PORT,
                properties = {
                    "okta.client.token=FAKE_TEST_TOKEN",
                    "okta.oauth2.localTokenValidation=false",
                    "okta.oauth2.discoveryDisabled=true",
                    "okta.client.orgUrl=http://localhost:${wiremock.server.port}",
                    "okta.oauth2.issuer=http://localhost:${wiremock.server.port}/oauth/issuer",
                    "security.oauth2.resource.userInfoUri=http://localhost:${wiremock.server.port}/oauth/userInfoUri"
                })
public class HoldingsApiIT {
Enter fullscreen mode Exit fullscreen mode

The above is a JUnit test configured to run with the SpringRunner. The ContextConfiguration annotation defines a RandomPortInitializer which will assign a random port to the property wiremock.server.port before the test runs. This is done so the auto-configured Okta Client and OAuth components will access a local WireMock server (by setting the above properties).

WireMock if you haven't already guessed is a nifty little testing lib you can use to mock HTTP responses, it spins up an embedded Jetty instance to handle real HTTP requests/responses. For example, if you wanted to mock a response to /my/resource and return the XML content <hello>still using XML</hello>, it would be represented in the following syntax:

stubFor(get(urlEqualTo("/my/resource"))
    .withHeader("Accept", equalTo("text/xml"))
    .willReturn(aResponse()
        .withStatus(200)
        .withHeader("Content-Type", "text/xml")
        .withBody("<hello>still using XML</hello>")));
Enter fullscreen mode Exit fullscreen mode

In simple cases, you can also use a JUnit rule to automatically start/stop the WireMock server. However, when coupled with the SpringRunner it doesn't work as well, do to the order in which rules and field injection happens. To work around this we simply fall back to the time-tested @Before and @After test annotations.

private WireMockServer wireMockServer;

@Before
public void startMockServer() throws IOException {
    wireMockServer = new WireMockServer(wireMockConfig().port(mockServerPort));
    configureWireMock();
    wireMockServer.start();
}

@After
public void stopMockServer() {
    if (wireMockServer != null && wireMockServer.isRunning()) {
        wireMockServer.stop();
    }
}
Enter fullscreen mode Exit fullscreen mode

I've also defined a method configureWireMock() which will configure how WireMock responds to requests:

private void configureWireMock() throws IOException {
     // load a JSON file from the classpath
    String body = StreamUtils.copyToString(getClass().getResourceAsStream("/its/user.json"), StandardCharsets.UTF_8);

    // respond to GET for user
    wireMockServer.stubFor(WireMock.get("/api/v1/users/" + TEST_USER_EMAIl)
            .willReturn(aResponse().withBody(body)));

    // respond to PUT for user
    wireMockServer.stubFor(WireMock.put("/api/v1/users/" + TEST_USER_ID)
            .willReturn(aResponse().withBody(body)));

    // OAuth userInfoUri
    String userInfoBody = StreamUtils.copyToString(getClass().getResourceAsStream("/its/userInfo.json"), StandardCharsets.UTF_8);
    wireMockServer.stubFor(
            WireMock.get("/oauth/userInfoUri")
                .withHeader("Authorization", WireMock.equalTo("Bearer "+ TEST_ACCESS_TOKEN))
            .willReturn(aResponse()
                    .withBody(userInfoBody)
                    .withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
            ));
}
Enter fullscreen mode Exit fullscreen mode

The above method loads a JSON file from the classpath /its/user.json and returns its contents for GET and PUT requests for the /api/v1/users endpoints. A similar response is mocked for a GET request to /oauth/userInfoUri.

The rest of test code looks pretty simple:

@Autowired
private TestRestTemplate restTemplate;

@Test
public void testGetHoldings() {

    List<Holding> holdings = Arrays.asList(restTemplate.getForObject("/api/holdings", Holding[].class));

    // use Spotify's hamcrest-pojo to validate the objects
    assertThat(holdings, contains(
            pojo(Holding.class)
                .withProperty("crypto",   is("crypto-1"))
                .withProperty("currency", is("currency-1"))
                .withProperty("amount",   is("amount-1")),
            pojo(Holding.class)
                .withProperty("crypto",   is("crypto-2"))
                .withProperty("currency", is("currency-2"))
                .withProperty("amount",   is("amount-2"))
    ));
}
Enter fullscreen mode Exit fullscreen mode

It's worth noting that this integration test is more complicated and slower than the corresponding unit test above. I feel developers typically create integration test because these tests have fewer lines of code. The unit tests finish nearly instantly, each integration test, on the other hand, spins up two servlet containers (one for our application and another for WireMock), for every test! Having both unit and integration tests is essential, you should be writing more unit tests than integration tests. Check out The Practical Test Pyramid to read more on the topic.

Use Spring MVC Test's MockMVC Instead

As I mentioned above, instead of using WireMock to fake an OAuth response, you can skip the OAuth validation check with a few clever tricks.

I'm not going to go into detail here, if you’re interested you can take a look how I wrote the same test with a different technique. The key areas to look at are the:

  • TestResourceServerConfiguration disables stateless configuration for the resource server (NOT recommended for production)
  • @WithMockUser(username=TEST_USER_ID) instructs the test framework to create a Spring SecurityContext for you
  • How MockMvc is created with apply(springSecurity())
  • Use with(securityContext(SecurityContextHolder.getContext())) when calling MockMvc.perform()

Add Unit Tests for Your Ionic / Angular Components

Ionic 3.x is built on Angular. This means that you should be able to use Angular testing tools to verify your app works as expected. When I create Angular apps, I tend to use Angular CLI. Angular CLI has a testing framework (Jasmine) built-in, as well as facilities to test via the command line or in a continuous integration server. If you're interested in learning more about how to write tests for an Angular app, I invite you to visit the testing section of my Angular CLI tutorial. Tip: it's easier on the eyes to read the testing tutorial on DocGist.

Ionic CLI doesn't contain a test command as Angular CLI does. To solve this problem, I turned to Daniel Sogl's ionic-super-starter. It uses Jest for unit tests, mocks for many Ionic classes, and has end-to-end testing support too.

I learned how to add testing support to an Ionic app by creating an app with Daniel's starter:

ionic start ionic-super-starter danielsogl/super
Enter fullscreen mode Exit fullscreen mode

Then, I copied the configuration into the crypto-pwa project. I've abbreviated those steps below.

1) Add scripts in the scripts section of your package.json to run unit tests and end-to-end (e2e) tests:

"test": "jest",
"test:watch": "jest --watch",
"test:ci": "jest --runInBand",
"test:coverage": "jest --coverage",
"e2e": "npm run e2e-update && npm run e2e-test",
"e2e-test": "protractor ./test/protractor.conf.js",
"e2e-update": "webdriver-manager update --standalone false --gecko false"
Enter fullscreen mode Exit fullscreen mode

2) Add the following dependencies to the devDependencies section of package.json:

"typescript": "2.8.1",
"@types/jest": "22.2.3",
"@types/node": "9.6.5",
"html-loader": "0.5.1",
"ionic-mocks-jest": "1.0.3",
"istanbul-instrumenter-loader": "3.0.0",
"jasmine": "3.1.0",
"jasmine-spec-reporter": "4.1.0",
"jest": "22.4.3",
"jest-preset-angular": "5.2.1",
"null-loader": "0.1.1",
"protractor": "5.1.1",
"ts-loader": "4.2.0",
"ts-node": "6.0.0"
Enter fullscreen mode Exit fullscreen mode

3) Add configuration for Jest, on the same level as devDependencies and scripts in package.json:

"jest": {
  "preset": "jest-preset-angular",
  "setupTestFrameworkScriptFile": "<rootDir>/test/jest.conf.ts",
  "transformIgnorePatterns": [
    "node_modules/(?!@ngrx|@ionic-native|@ionic)"
  ],
  "coveragePathIgnorePatterns": [
    "<rootDir>/e2e/"
  ],
  "collectCoverageFrom": [
    "**/*.{ts}",
    "!**/*d.{ts}",
    "!**/*interface.{ts}",
    "!**/*module.{ts}",
    "!**/*main.{ts}",
    "!**/node_modules/**"
  ],
  "moduleFileExtensions": [
    "ts",
    "js",
    "json"
  ],
  "coverageReporters": [
    "json",
    "lcov",
    "text"
  ],
  "coverageDirectory": "./public/coverage"
}
Enter fullscreen mode Exit fullscreen mode

4) Create a crypto-pwa/test directory and create two files in it: jest.conf.ts and protractor.conf.js.

// test/jest.conf.ts
import 'jest-preset-angular';
Enter fullscreen mode Exit fullscreen mode
// test/protractor.conf.js
const { SpecReporter } = require('jasmine-spec-reporter');

exports.config = {
  allScriptsTimeout: 11000,
  specs: [
    '../e2e/**/*.e2e-spec.ts'
  ],
  capabilities: {
    'browserName': 'chrome'
  },
  directConnect: true,
  baseUrl: 'http://localhost:8100/',
  framework: 'jasmine',
  jasmineNodeOpts: {
    showColors: true,
    defaultTimeoutInterval: 30000,
    print: function() {}
  },
  onPrepare() {
    require('ts-node').register({
      project: 'e2e/tsconfig.e2e.json'
    });
    jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
  }
};
Enter fullscreen mode Exit fullscreen mode

5) Create src/tsconfig.spec.json and populate it with the following JSON:

{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "outDir": "../out-tsc/spec",
    "module": "commonjs",
    "target": "es5",
    "allowJs": true
  },
  "include": [
    "**/*.spec.ts"
  ],
  "exclude": [
    "**/*e2e-spec.ts"
  ]
}
Enter fullscreen mode Exit fullscreen mode

6) Create e2e/tsconfig.e2e.json and configure its settings:

{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "outDir": "../out-tsc/e2e",
    "baseUrl": "./",
    "module": "commonjs",
    "target": "es5",
    "typeRoots": [
      "../node_modules/@types"
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

NOTE: After making all these changes, I updated all dependencies in crypto-pwa/package.json by installing npm-check-updates and running ncu -u.

7) Run npm i to install the new dependencies you added.

Add Unit Tests for Ionic Components with Jest

According to the Jest homepage, it's used by Facebook to test all JavaScript code including React applications. Jest strives for zero-configuration, but you can tell from the files above that it still requires some configuration. It does have built-in code coverage reports, which is kinda cool.

If you're new to Jest and want to learn the basics, I invite you to read its Getting Started guide.

Jest is similar to Karma and Jasmine. Karma is a test runner, and Jasmine is a behavior-driven development framework for testing JavaScript code. For Java developers, Jasmine is like the JUnit of the JavaScript ecosystem. A simple Jasmine test looks as follows:

describe('A suite is just a function', () => {
  let a;

  it('and so is a spec', () => {
    a = true;

    expect(a).toBe(true);
  });
});
Enter fullscreen mode Exit fullscreen mode

Jest tests look very similar, but there are some slight differences. If you're interested in learning more, I'd suggest googling "jest vs jasmine."

After adding Jest support to your Ionic project, you have several options to run unit tests:

  1. Run npm test to execute all the unit tests.
  2. Run npm run test:watch to execute tests and watch for changes. If anything changes, tests are automatically executed.
  3. Run npm test:ci to execute tests in a continuous integration (CI) server. This runs all tests serially in the current process, rather than creating a worker pool of child processes that run tests.
  4. Run npm run test:coverage to generate a test coverage report.

These commands are great to know, but only useful if you have unit tests!

Create crypto-pwa/src/app/app.component.spec.ts and populate it the following code:

import { async, TestBed } from '@angular/core/testing';
import { IonicModule, Platform } from 'ionic-angular';
import { StatusBar } from '@ionic-native/status-bar';
import { SplashScreen } from '@ionic-native/splash-screen';
import { PlatformMock, SplashScreenMock, StatusBarMock } from 'ionic-mocks-jest';
import { MyApp } from './app.component';
import { OAuthService } from 'angular-oauth2-oidc';

describe('MyApp Component', () => {
  let fixture;
  let component;
  let oauthService = {
    hasValidIdToken() {
      return false;
    }
  };

  beforeEach(
    async(() => {
      TestBed.configureTestingModule({
        declarations: [MyApp],
        imports: [IonicModule.forRoot(MyApp)],
        providers: [
          {provide: StatusBar, useFactory: () => StatusBarMock.instance()},
          {provide: SplashScreen, useFactory: () => SplashScreenMock.instance()},
          {provide: Platform, useFactory: () => PlatformMock.instance()},
          {provide: OAuthService, useFactory: () => oauthService}
        ]
      });
    })
  );

  beforeEach(() => {
    fixture = TestBed.createComponent(MyApp);
    component = fixture.componentInstance;
  });

  it('should be created', () => {
    expect(component instanceof MyApp).toBe(true);
  });

  it('should show login page', () => {
    expect(component.rootPage).toEqual('LoginPage');
  });
});
Enter fullscreen mode Exit fullscreen mode

This test will pass, but a lot is going on, so let me break it down for you. First of all, you might notice there are some imports from ionic-mocks-jest. If generate an Ionic app using ionic start and select the "tabs" starter, you'll have a MyApp component that looks as follows:

export class MyApp {
  rootPage:any = TabsPage;

  constructor(platform: Platform, statusBar: StatusBar, splashScreen: SplashScreen) {
    platform.ready().then(() => {
      // Okay, so the platform is ready and our plugins are available.
      // Here you can do any higher level native things you might need.
      statusBar.styleDefault();
      splashScreen.hide();
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Whenever a component has dependencies defined in its constructor, you need to "provide" those in your component's test.

To make it easy to components that depend on Ionic components, Daniel Sogl forked the ionic-mocks project to create ionic-mocks-jest. This project provides many mock objects using Jasmine Spy Objects, with support for Jest. A spy can stub any function and tracks calls to it, as well as all the arguments passed in. You can imagine how this can be useful, especially when testing.

The crypto-pwa project is meant to a be a PWA, not a native app, so these dependencies don't exist in its MyApp component. However, I thought I'd point them out in case your app uses them.

There's a bit of boilerplate code to test an Angular component. If I were to simplify the test above only to have the essentials for a bare-bones component and test, it'd look like the following:

describe('MyApp Component', () => {
  let fixture;
  let component;

  beforeEach(
    async(() => {
      TestBed.configureTestingModule({
        declarations: [MyApp]
      });
    })
  );

  beforeEach(() => {
    fixture = TestBed.createComponent(MyApp);
    component = fixture.componentInstance;
  });

  it('should show login page', () => {
    expect(component.rootPage).toEqual('LoginPage');
  });
});
Enter fullscreen mode Exit fullscreen mode

This test will fail because the MyApp component loads app.html with Ionic elements in it.

 FAIL  src/app/app.component.spec.ts
  MyApp Component
    ✕ should be created (647ms)
    ○ skipped 1 test

  ● MyApp Component › should be created

    Template parse errors:
    Can't bind to 'root' since it isn't a known property of 'ion-nav'.
    1. If 'ion-nav' is an Angular component and it has 'root' input, t
Enter fullscreen mode Exit fullscreen mode

Import IonicModule and you'll get a bit further. This is a common pattern. If you have custom components in your templates, you'll need to import their modules in your tests, just like you do in app.module.ts.

beforeEach(
  async(() => {
    TestBed.configureTestingModule({
      declarations: [MyApp],
      imports: [IonicModule.forRoot(MyApp)]
    });
  })
);
Enter fullscreen mode Exit fullscreen mode

If you try to run the test now, it'll complain that the constructor dependency in MyApp is not available.

StaticInjectorError(DynamicTestModule)[MyApp -> OAuthService]:
  StaticInjectorError(Platform: core)[MyApp -> OAuthService]:
    NullInjectorError: No provider for OAuthService!
Enter fullscreen mode Exit fullscreen mode

To fix this, create a mock instance of oauthService and specify it as a provider.

const oauthService = {
  hasValidIdToken() {
    return false;
  }
};

beforeEach(
  async(() => {
    TestBed.configureTestingModule({
      declarations: [MyApp],
      imports: [IonicModule.forRoot(MyApp)],
      providers: [
        {provide: OAuthService, useFactory: () => oauthService}
      ]
    });
  })
);
Enter fullscreen mode Exit fullscreen mode

TIP: You could also use {provide: OAuthService, useValue: oauthService} to substitute your mock OAuthService for the real one. Another option is useClass, which allows you to specify a different class.

You only need to define the hasValidIdToken() in oauthService because that's the only method that's used in MyApp:

import { Component } from '@angular/core';
import { OAuthService } from 'angular-oauth2-oidc';

@Component({
  templateUrl: 'app.html'
})
export class MyApp {
  rootPage: any = 'HomePage';

  constructor(oauthService: OAuthService) {
    if (oauthService.hasValidIdToken()) {
      this.rootPage = 'HomePage';
    } else {
      this.rootPage = 'LoginPage';
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Now you can create a test that ensures the login page is shown when no ID token exists.

it('should show login page', () => {
  expect(component.rootPage).toEqual('LoginPage');
});
Enter fullscreen mode Exit fullscreen mode

Run npm test and everything should pass!

> jest

 PASS  src/app/app.component.spec.ts
  MyApp Component
    ✓ should be created (517ms)
    ✓ should show login page (330ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        4.363s, estimated 5s
Ran all test suites.
Enter fullscreen mode Exit fullscreen mode

NOTE: You might experience the following test failure:

Cannot find module 'rxjs-compat/Observable' from 'Observable.js'
Enter fullscreen mode Exit fullscreen mode

If this happens, it's because running ncu -u updated RxJS from 5.5.8 to 6.x. To fix it, you can modify your package.json to revert to 5.5.10 (the latest version) or run npm i -rxjs-compat. See the version 6 migration guide for more information.

The HomeComponent loads the list of holdings a user has after they authenticate in its ionViewDidLoad method.

ionViewDidLoad(): void {
  if (!this.oauthService.hasValidIdToken()) {
    this.navCtrl.push('LoginPage');
  }
  this.holdingsProvider.loadHoldings();
}
Enter fullscreen mode Exit fullscreen mode

To test that everything works as expected, create crypto-pwa/src/pages/home/home.spec.ts with stubs for its providers.

import { IonicModule, NavController } from 'ionic-angular';
import { OAuthService } from 'angular-oauth2-oidc';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { HomePage } from './home';
import { HoldingsProvider } from '../../providers/holdings/holdings';
import { By } from '@angular/platform-browser';

describe('HomePage', () => {
  let fixture: ComponentFixture<HomePage>;
  let component: HomePage;
  let oauthService = {
    hasValidIdToken() {
      return true;
    },
    getIdentityClaims() {}
  };
  let holdingsProvider = {
    holdings: [{crypto: 'BTC', currency: 'USD', amount: 5, value: '10000'}],
    loadHoldings() {
      return this.holdings;
    }
  };
  let loadHoldings, getIdentityClaims;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [HomePage],
      imports: [IonicModule.forRoot(HomePage)],
      providers: [NavController,
        {provide: OAuthService, useValue: oauthService},
        {provide: HoldingsProvider, useValue: holdingsProvider}
      ]
    });
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(HomePage);
    component = fixture.componentInstance;
    loadHoldings = jest.spyOn(holdingsProvider, 'loadHoldings');
    getIdentityClaims = jest.spyOn(oauthService, 'getIdentityClaims');
  });

  it('should be created', () => {
    expect(component).toBeDefined()
  });
});
Enter fullscreen mode Exit fullscreen mode

This test uses jest.spyOn to spy on the methods that should be called, and verify they were. If you want to mock functions and return fake data, you can do this with jest.fn() and mock functions.

Add tests to ensure HoldingsProvider.loadHoldings() is called and a list of currencies is shown.

it('should call loadHoldings', () => {
  component.ionViewDidLoad();
  fixture.detectChanges();
  expect(loadHoldings).toHaveBeenCalled();
  expect(getIdentityClaims).toHaveBeenCalled();
});

it('should show list of currencies', () => {
  component.ionViewDidLoad();
  fixture.detectChanges();
  const list: HTMLDivElement = fixture.debugElement.query(By.css('ion-list')).nativeElement;
  expect(list.innerHTML).toMatch(/ion-item/);
  const amount = fixture.debugElement.query(By.css('.amount')).nativeElement;
  expect(amount.innerHTML).toMatch(/<strong>Coins:<\/strong> 5 <strong>Value:<\/strong> 10000/)
});
Enter fullscreen mode Exit fullscreen mode

Run npm test and everything should pass.

Test Suites: 2 passed, 2 total
Tests:       5 passed, 5 total
Snapshots:   0 total
Time:        5.98s
Enter fullscreen mode Exit fullscreen mode

Rather than showing you how to unit test every component, I'll show you how to write one more: a provider test. To test
providers (often called services in regular Angular apps) that use HttpClient, you can use HttpTestingController. Create crypto-pwa/src/providers/holdings/holdings.spec.ts and populate it with the code below.

import { getTestBed, TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { HoldingsProvider } from './holdings';
import { OAuthService } from 'angular-oauth2-oidc';

describe('HoldingsProvider', () => {
  let injector: TestBed;
  let provider: HoldingsProvider;
  let httpMock: HttpTestingController;
  let oauthService = {
    hasValidIdToken() {
      return true;
    },
    authorizationHeader() {
      return "random-string";
    }
  };

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [HoldingsProvider,
        {provide: OAuthService, useValue: oauthService}
      ]
    });

    injector = getTestBed();
    provider = injector.get(HoldingsProvider);
    httpMock = injector.get(HttpTestingController);
  });

  afterEach(() => {
    httpMock.verify()
  });

  it('should be created', () => {
    expect(provider).toBeTruthy();
  });

  it('should retrieve holdings', () => {
    const fakeHoldings = [
      {crypto: 'BTC', currency: 'USD', amount: 5, value: '10000'},
      {crypto: 'ETH', currency: 'USD', amount: 100, value: '700'}
    ];

    provider.loadHoldings();
    const req = httpMock.expectOne(provider.HOLDINGS_API);
    expect(req.request.method).toBe('GET');
    req.flush(fakeHoldings);

    expect(provider.holdings.length).toBe(2);
    expect(provider.holdings[0].crypto).toBe('BTC');
    expect(provider.holdings).toEqual(fakeHoldings);

    // calls to get prices
    httpMock.expectOne('https://api.cryptonator.com/api/ticker/BTC-USD');
    httpMock.expectOne('https://api.cryptonator.com/api/ticker/ETH-USD');
  });
});
Enter fullscreen mode Exit fullscreen mode

The real action happens via the following steps:

  1. The provider's method is called, loadHoldings() in this case
  2. The request is mocked using expectOne()
  3. Flush the request passing fake values

After this happens, you can set your expectations accordingly. If there are any extra calls (like fetchPrices()) in your provider, you'll need to set expectations for those too. The afterEach function is used to verify there are no unanticipated HTTP calls.

HAT TIP: I learned how to test providers that use HttpClient from Ciro Nunes' Testing with the Angular HttpClient API.

Run npm test to verify all your unit tests pass.

Now that you have unit tests, add some end-to-end tests that ensure the whole app works as expected.

Drive and Test Your Ionic PWA with Protractor

Protractor recommends using Page Objects to organize tests. Page Objects help you write cleaner tests by encapsulating information about a page's elements in a single object.

Setup a basic e2e test of your application's homepage with the following steps:

1) Create a generic Page object at crypto-pwa/e2e/pages/app.po.ts:

import { browser } from 'protractor';

export class Page {

  navigateTo(destination) {
    return browser.get(destination);
  }

  getTitle() {
    return browser.getTitle();
  }
}
Enter fullscreen mode Exit fullscreen mode

2) Create crypto-pwa/e2e/spec/app.e2e-spec.ts and define a "default screen" test suite:

import { Page } from '../pages/app.po';
import { browser, ExpectedConditions as ec } from 'protractor';

describe('App', () => {
  let page: Page;

  beforeEach(() => {
    page = new Page();
  });

  describe('default screen', () => {
    beforeEach(() => {
      page.navigateTo('/#/home');
    });

    it('should redirect to login', () => {
      browser.wait(ec.urlContains('/#/login'), 5000);
    });

    it('should have the correct title', () => {
      page.getTitle().then(title => {
        expect(title).toEqual('Cryptocurrency PWA with Authentication');
      });
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

NOTE: In order for getLoginButton() to work, you'll need to add id="login" to the login button in crypto-pwa/src/pages/login/login.html.

To execute Protractor tests, run ionic serve in one terminal and npm run e2e in another. This (silent) video shows what should happen.

You can also configure Protractor to launch its own web server. To do this, add a devDependency on node-http-server:

npm i -D node-http-server@8.1.2
Enter fullscreen mode Exit fullscreen mode

Then modify crypto-pwa/test/protractor.conf.js to serve up the www directory on port 8100.

const server = require('node-http-server');

exports.config = {
  ...
  onPrepare() {
    require('ts-node').register({
      project: 'e2e/tsconfig.e2e.json'
    });
    server.deploy({port: 8100, root: 'www'});
    jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
  }
};
Enter fullscreen mode Exit fullscreen mode

The only drawback to this technique is you'll have to build your project (with npm run build so the www is populated) before running it. It's a good idea to do a prod build (npm run build --prod) before running e2e tests anyway, so this drawback can also be viewed as a good practice.

Even better, you can automate it with continuous integration! We'll get to that in a minute. First, create a crypto-pwa/e2e/pages/login.po.ts that defines the elements and methods you'll need to authenticate.

import { browser, by, element } from 'protractor';
import { Page } from './app.po';

export class LoginPage extends Page {
  username = element(by.name('username'));
  password = element(by.name('password'));
  oktaLoginButton = element(by.css('input[type=submit]'));
  loginButton = element(by.css('#login'));
  logoutButton = element(by.css('#logout'));
  header = element(by.css('ion-title'));

  getHeader() {
    return this.header.getText();
  }

  setUserName(username) {
    this.username.sendKeys(username);
  }

  getUserName() {
    return this.username.getAttribute('value');
  }

  clearUserName() {
    this.username.clear();
  }

  setPassword(password) {
    this.password.sendKeys(password);
  }

  getPassword() {
    return this.password.getAttribute('value');
  }

  clearPassword() {
    this.password.clear();
  }

  login(username: string, password: string) {
    // Entering non angular site, tell webdriver to switch to synchronous mode.
    browser.waitForAngularEnabled(false);
    this.username.isPresent().then(() => {
      this.username.sendKeys(username);
      this.password.sendKeys(password);
      this.oktaLoginButton.click();
    }).catch(error => {
      browser.waitForAngularEnabled(true);
    });
  }

  clickLoginButton() {
    return this.loginButton.click();
  }

  logout() {
    return this.logoutButton.click();
  }
}
Enter fullscreen mode Exit fullscreen mode

Then create crypto-pwa/e2e/spec/login.e2e-spec.ts with tests that ensure a login button exists, that invalid credentials results in an error, valid credentials shows a welcome message, and that you can log out successfully.

import { browser, element, by, ExpectedConditions as ec } from 'protractor';
import { LoginPage } from '../pages/login.po';

describe('Login', () => {

  let loginPage;

  beforeAll(() => {
    loginPage = new LoginPage();
    loginPage.navigateTo('/');
    browser.waitForAngular();
  });

  it('should show a login button', () => {
    expect(loginPage.getHeader()).toMatch(/Login/);
    expect(loginPage.loginButton.isPresent());
  });

  it('should fail to log in with bad password', () => {
    loginPage.clickLoginButton();
    loginPage.login('admin', 'foo');
    const error = element.all(by.css('.infobox-error')).first();
    browser.wait(ec.visibilityOf(error), 2000).then(() => {
      expect(error.getText()).toMatch("Sign in failed!");
    });
  });

  it('should log in successfully with demo account', () => {
    loginPage.clearUserName();
    loginPage.setUserName(process.env.E2E_USERNAME);
    loginPage.clearPassword();
    loginPage.setPassword(process.env.E2E_PASSWORD);
    loginPage.oktaLoginButton.click();

    const welcome = /Welcome/; // Use /Welcome, First Last/ if you want to verify full name
    const success = element.all(by.css('h1')).first();
    browser.wait(ec.visibilityOf(success), 5000).then(() => {
      expect(success.getText()).toMatch(welcome);
    });
  });

  it('should log out successfully', () => {
    loginPage.logout();
    browser.wait(ec.urlContains('/#/login'), 2000);
    expect(loginPage.loginButton.isPresent());
  })
});
Enter fullscreen mode Exit fullscreen mode

The process.env.* variables provide a convenient way to store credentials in environment variables, so you're not exposing them in your source control system. You'll need to define E2E_USERNAME and E2E_PASSWORD environment variables for this test to pass. The values should match valid credentials in your Okta tenant.

After you've defined them and verified they're set properly, start the Spring Boot app using mvn in one terminal, then run the following commands in the crypto-pwa directory.

npm run build --prod
npm run e2e
Enter fullscreen mode Exit fullscreen mode

When everything passes, pat yourself on the back; you're well on your way to being an experience TypeScript tester!

TIP: Protractor uses Jasmine, which allows you to easily include/exclude specific tests from running. You can add an f prefix to your describe or it functions, so they become fdescribe or fit. If you want to exclude tests, you can add an x prefix to these same code blocks (e.g., xdescribe and xit).

One of the problems with the e2e tests you’ve written so far is they don’t use the API to save holdings in Okta. To create a test that exercises that functionality, create a crypto-pwa/e2e/pages/add-holding.po.ts page object for Add Holding page. This class has convenience methods to fill out the form fields and click the submit button.

import { $, by, element } from 'protractor';
import { Page } from './app.po';

export class AddHoldingPage extends Page {
  cryptoCode = element.all(by.css('input[type=text]')).first();
  displayCurrency = element.all(by.css('input[type=text]')).get(1);
  amountHolding = element.all(by.css('input[type=number]'));
  addHoldingButton = element(by.buttonText('Add Holding'));
  pageTitle = $('ion-title');

  setCryptoCode(code) {
    this.cryptoCode.sendKeys(code);
  }

  setCurrency(currency) {
    this.displayCurrency.sendKeys(currency);
  }

  setAmount(amount) {
    this.amountHolding.sendKeys(amount);
  }

  clickAddHoldingButton() {
    this.addHoldingButton.click();
  }

  getPageTitle() {
    return this.pageTitle;
  }
}
Enter fullscreen mode Exit fullscreen mode

Add a crypto-pwa/e2e/pages/home.po.ts for the Home page that exposes its Add Coins button and delete button for holdings.

import { by, element } from 'protractor';
import { Page } from './app.po';

export class HomePage extends Page {
  addCoinsButton = element(by.buttonText('Add Coins'));
  deleteButton = element.all(by.css('button[color=danger]')).last();

  clickAddCoinsButton() {
    this.addCoinsButton.click();
  }
}
Enter fullscreen mode Exit fullscreen mode

Now that you have these page objects in place, you can add a new e2e test that logs in, adds three Bitcoins in USD, and then deletes the holding. The hardest part in writing this test was figuring out how to swipe left with Protractor, so I’d recommend making note of how that’s done.

import { browser, by, element, ExpectedConditions as ec } from 'protractor';
import { LoginPage } from '../pages/login.po';
import { AddHoldingPage } from '../pages/add-holding.po';
import { HomePage } from '../pages/home.po';

describe('Manage Holdings', () => {

  let loginPage, homePage, addHoldingPage;

  beforeAll(() => {
    loginPage = new LoginPage();
    homePage = new HomePage();
    addHoldingPage = new AddHoldingPage();
    loginPage.navigateTo('/');
    browser.waitForAngular();
  });

  beforeEach(() => {
    loginPage.clickLoginButton();
    loginPage.login(process.env.E2E_USERNAME, process.env.E2E_PASSWORD);
    loginPage.oktaLoginButton.click();

    browser.wait(ec.urlContains('home'), 5000);
  });

  afterEach(() => {
    loginPage.logout();
  });

  it('should add and remove a holding', () => {
    homePage.clickAddCoinsButton();

    browser.wait(ec.urlContains('add-holding'), 1000);

    addHoldingPage.setCryptoCode('BTC');
    addHoldingPage.setCurrency('USD');
    addHoldingPage.setAmount(3);
    addHoldingPage.clickAddHoldingButton();

    // wait for everything to happen
    browser.wait(ec.urlContains('home'), 5000);

    // verify message is removed and holding shows up
    element.all(by.css('.message')).then((message) => {
      expect(message.length).toBe(0);
    });

    // wait for holding to show up
    const addedHolding = element.all(by.css('ion-item')).last();
    browser.wait(ec.presenceOf(addedHolding), 5000).then(() => {

      // delete the holding - https://forum.ionicframework.com/t/move-ion-item-sliding-by-protractor/106918
      browser.actions().mouseDown(addedHolding)
        .mouseMove({x: -50, y: 0})
        .mouseMove({x: -50, y: 0})
        .mouseMove({x: -50, y: 0})
        .mouseUp()
        .perform();

      homePage.deleteButton.click();
      element.all(by.css('.message')).then((message) => {
        expect(message.length).toBe(1);
      });
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

See okta-ionic-crypto-java-sdk-example#5 for a pull request that contains all UI tests.

Add Continuous Integration with Travis CI

Having tests is great, but you know what's better? Running them every time a pull request (PR) is created for your project. You do use Git to store your projects in source control, don't you?! I'll assume you do. I mean, I know you're a smart developer if you're reading this. Friends don't let friends write authentication, and you're here, so you don't have to, right? 😉

Two of the most popular CI servers are Travis CI and Jenkins. Since Travis doesn't require any local setup, we'll wrap this post up with a quick overview of how to use it.

If you've checked your project into GitHub, you can use Travis CI.

  1. Log in to Travis CI and enable builds for your project’s GitHub repo.
  2. Add the following .travis.yml in your root directory, create a branch for it (e.g., git branch -b ci), and git commit/push it.
os:
  - linux
services:
  - docker
language: node_js
node_js:
  - "9.8.0"
addons:
  apt:
    sources:
    - google-chrome
    packages:
    - google-chrome-stable
jdk:
  - oraclejdk8
sudo: false
cache:
  directories:
    - crypto-pwa/node_modules
before_install:
  - jdk_switcher use oraclejdk8
  - java -version
  - export CHROME_BIN=/usr/bin/google-chrome
  - export DISPLAY=:99.0
  - sh -e /etc/init.d/xvfb start
  - npm install -g ionic@3.20.0
script:
  - chmod +x holdings-api/mvnw
  - cd holdings-api && ./mvnw -q clean verify
  - cd ../crypto-pwa && npm i && npm test
  - cd ../holdings-api && mvnw -q spring-boot:run &
  - cd ../crypto-pwa && npm run build --prod
  - npm run e2e
notifications:
  webhooks:
    on_success: always
    on_failure: always
    on_start: false
Enter fullscreen mode Exit fullscreen mode

3) Navigate to your GitHub repo in a browser and create a pull request with the branch you just pushed. This will kick off processes in Travis to test your branch.

The .travis.yml script above will run your Spring Boot tests with Java 8, run the Jest tests, start the backend, start the frontend, and then run Protractor tests to verify everything works.

NOTE: You might notice that the Java unit tests and integration tests run with ./mvnw verify, rather than as two separate commands. This is because mvn test only runs unit tests and mvn verify runs both unit tests and integration tests.

When Travis first runs this script, you'll likely see an error like the one below.

Travis failure

This happens because the E2E_USERNAME and E2E_PASSWORD environment variables aren't set. Travis makes it easy to set encrypted environment variables. Perform the steps below to set these up for your build.

  1. Install the travis gem using gem install travis.
  2. In the root directory of your repo, run the following command to create encrypted environment variables for the username, password, and the API token. This command assumes you already have these values defined locally.
travis encrypt E2E_USERNAME=$E2E_USERNAME \
    E2E_PASSWORD=$E2E_PASSWORD OKTA_CLIENT_TOKEN=$OKTA_CLIENT_TOKEN --add env.matrix
Enter fullscreen mode Exit fullscreen mode

3) Commit and push this change, and your next build should succeed.

You can see a successful build in the screenshot below.

Travis success

See okta-ionic-crypto-java-sdk-example#4 for a pull request that adds Travis configuration.

We encourage you to read Travis CI's Best Practices in Securing Your Data to learn more about how to avoid leaking secrets.

Add Code Coverage Reports

Reporting how much of your code is tested is an excellent way to see how much technical debt you have and the parts of your application that need more tests.

Add the JaCoCo Java Code Coverage Library

For Java code coverage I typically use JaCoCo. It works by adding a Java agent, -javaagent:/path/to/jacocoagent.jar but since it has plugins for all of the major build tools you typically never need to do this directly. For example, I've configured this project to report UT, IT and total test coverage with the following pom.xml snippet.

<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.1</version>
    <executions>
        <execution>
            <id>prepare-agent</id>
            <phase>process-test-classes</phase>
            <goals>
                <goal>prepare-agent</goal>
            </goals>
        </execution>
        <execution>
            <id>prepare-agent-integration</id>
            <phase>pre-integration-test</phase>
            <goals>
                <goal>prepare-agent-integration</goal>
            </goals>
        </execution>
        <execution>
            <id>jacoco-report</id>
            <phase>verify</phase>
            <goals>
                <goal>report</goal>
                <goal>report-integration</goal>
            </goals>
        </execution>
        <execution>
            <id>jacoco-merge-report</id>
            <phase>verify</phase>
            <goals>
                <goal>merge</goal>
                <goal>report</goal>
            </goals>
            <configuration>
                <dataFile>${project.build.directory}/jacoco-merge.exec</dataFile>
                <outputDirectory>${project.reporting.outputDirectory}/jacoco-merge</outputDirectory>
                <destFile>${project.build.directory}/jacoco-merge.exec</destFile>
                <fileSets>
                  <fileSet>
                    <directory>${project.build.directory}</directory>
                    <includes>
                      <include>*.exec</include>
                    </includes>
                  </fileSet>
                </fileSets>
            </configuration>
        </execution>
    </executions>
</plugin>
Enter fullscreen mode Exit fullscreen mode

The last bit here with the id of jacoco-merge-report merges the UT, and IT reports to create a new report with the total project test coverage. If you are working with a Maven multi-module build, it is a little easier then this, and you could use the report-aggregate goal instead.

TypeScript Code Coverage with Jest and Istanbul

For the Angular side of things, Jest has built-in code coverage. You merely have to run npm run test:coverage. You can see from the screenshot below that there's still a couple of pages that need more tests.

Jest coverage

Istanbul is another favorite code coverage tool in the JavaScript ecosystem. It's used by Angular CLI. If you're in an Angular CLI-generated project, run ng test --sr --cc from the root of your project to generate a report in coverage/index.html.

Report Code Coverage with Travis

For Java code coverage, I've been using Codecov, mostly because it is so easy to use. In your travis.yml you just add a pointer to your coverage file:

after_success:
 - bash <(curl -s https://codecov.io/bash) -f holdings-api/target/site/jacoco-merge/jacoco.xml
Enter fullscreen mode Exit fullscreen mode

Codecov has excellent support for JavaScript and TypeScript too! To add Codecov to Travis for Jest, you need to configure your package.json to collect coverage.

"jest": {
  "coverageDirectory": "./coverage/",
  "collectCoverage": true
}
Enter fullscreen mode Exit fullscreen mode

Of course, you’ll need to configure Travis to install the codecov command line tool before you can use it. You can do that with npm i -g codecov or you can use bash <(curl -s https://codecov.io/bash) like Brian does in his example above.

install:
  - npm install -g codecov
Enter fullscreen mode Exit fullscreen mode

Then you can gather coverage information after everything succeeds.

after_success:
  - cd $TRAVIS_BUILD_DIR && codecov
Enter fullscreen mode Exit fullscreen mode

The codecov command is smart enough to find the jacoco.xml files on the Java side, as well as the lcov.info file created by Jest.

==> Scanning for reports
    + /home/travis/.../crypto-pwa/public/coverage/lcov.info
    + /home/travis/.../holdings-api/target/site/jacoco/jacoco.xml
    + /home/travis/.../holdings-api/target/site/jacoco-it/jacoco.xml
    + /home/travis/.../holdings-api/target/site/jacoco-merge/jacoco.xml
==> Uploading reports
    Success!
    View report at: https://codecov.io/gh/oktadeveloper/okta-ionic-crypto-java-sdk-example/tree/ee2382623ada72cd78188f4d25318dfaf3d24882
Enter fullscreen mode Exit fullscreen mode

Codecov report

TIP: Codecov allows you to add unittests, integration, and ui flags in your code to group types of tests.

You can configure Codecov to report on your GitHub pull requests too! If you don’t see your org listed on the previous page, you should be able to navigate directly to it. For example, this project’s direct link is https://codecov.io/gh/oktadeveloper/okta-ionic-crypto-java-sdk-example.

See okta-ionic-crypto-java-sdk-example#6 for a pull request that adds code coverage.

Learn More about Testing in Spring Boot and TypeScript Apps

We hope you've enjoyed this hitchhiker's guide to testing Java APIs and Ionic/Angular applications. It's an excellent feeling when your codebase has high test coverage, and you're able to refactor by tweaking your code and adjusting your tests.

You can find the code used in this guide on GitHub: https://github.com/oktadeveloper/okta-ionic-crypto-java-sdk-example.

If you'd like to learn more about testing Java and Spring Boot APIs, please see:

For Angular and Ionic testing here a few resources we found useful:

If you're into Ionic and JHipster, we recently added unit and e2e testing support to the Ionic Module for JHipster and its associated Ionic JHipster Starter. Both projects have some pretty sophisticated Travis builds that ensure everything works with JWT/OIDC authentication and a JHipster backend.

Speaking of complicated builds, JHipster's main build and additional build test over 54 combinations!

If you're looking for a Spring Boot + Angular (or React) example app with lots of tests, look no further than JHipster. It even has a continuous integration sub-generator that supports Jenkins, Travis, CircleCI, and GitLab CI/CD.

If you'd like to learn more about topics like Spring Boot, Angular, React, and JHipster, give us a follow @oktadev. If you have any questions about this post, please leave a comment below.

Changelog:

Top comments (0)