/test-jenk-unit

My journey into the land of Jenkins Pipeline Unit Testing.

Primary LanguageGroovy

jenkins pipeline unit test learning repo

This is the simplest of hello-world repos to get off the ground, and I can’t couldn't get past the first step.

References

Starting Point

After many false starts, I finally was able to put together this starter repo based on the tutorial above and give it a try.

./gradlew test should compile and run the test, but I get an ssl error:

> ./gradlew test
Downloading https://services.gradle.org/distributions/gradle-6.8.3-bin.zip

Exception in thread "main" javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to fi
nd valid certification path to requested target
        at sun.security.ssl.Alerts.getSSLException(Unknown Source)
        at sun.security.ssl.SSLSocketImpl.fatal(Unknown Source)

(most of the error messages clipped)

I get this even when off the VPN. So I figure my java cert chain is out of date. What’s the best way to fix it?

The Answer*

* to the SSL issue only, thank you drive through

I get the error when off VPN because the corporate Zscaler grabs all traffic from our laptops, regardless of VPN status. IT pushes certs to our Windows cert store in the background, but we're on our own for non-standard runtimes.

These steps are required to add the new Zscaler root CA cert to the Java keystore:

  1. Grab the Root ZScaler cert from a browser.

  2. Execute these commands in PowerShell:

    $caargs = @('-importcert',
      '-trustcacerts',
      '-alias', 'pick-a-good-alias',
      '-file', 'path/to/the/exported.cer',
      '-storepass', 'changeit', 
      '-keystore', '.\cacerts', 
      '-noprompt')
    
    Push-Location 'C:\Program Files (x86)\Java\jre1.8.0_241\lib\security'   
    
    ..\..\bin\keytool.exe @caargs

    Note the @ in the last line is not a typo, it is the splat operator to write the contents of the array as a single line.

Getting The Build To Work

After getting past the SSL issue, the code I'd copied wholesale from the tutorial still wouldn't compile, as it's several years old. I had some kind of mental breakthrough and remembered this other tutorial that talked about how to set up the directories correctly:

sourceSets {
  main {
    groovy {
      // all code files will be in either of the folders
      srcDirs = ['src', 'vars'] 
    }
  }

  test {
    groovy {
      srcDir 'test'
    }
  }
}

I fiddled with that, made some progress, but was still getting into dependency hell with the libraries. After a few iterations I landed on this, which combined the info from both walkthroughs:

repositories {
  jcenter()
    maven {
     url 'http://repository.apache.org/snapshots/'
     url 'https://repo.jenkins-ci.org/releases/'
  }
}

See build.gradle for the final file contents.

At last, a working build:

> ./gradlew test

 Task :test
ToAlphanumericTest: testCall: SUCCESS
Tests: 1, Failures: 0, Errors: 0, Skipped: 0

BUILD SUCCESSFUL in 5s
4 actionable tasks: 2 executed, 2 up-to-date

I'll be honest - I don't really understand the java specifics as deeply as I'd like to. But, it's compiling, it's testing, it's writing output - this is a huge step forward.

Going Deeper

Tweaking The Test Command

  • First off, I set an alias to speed up the ol' fingers:

    > set-alias -name gw -value './gradlew'
    
    > get-alias gw
    
    CommandType     Name
    -----------     ----
    Alias           gw -> gradlew.bat
  • gradlew --help shows a bunch of options.

    --console verbose provides some interesting details, including several tasks that have no code, and we might as well skip:

    > gw test --console verbose
    > Task :compileJava NO-SOURCE
    > Task :compileGroovy UP-TO-DATE
    > Task :processResources NO-SOURCE
    > Task :classes UP-TO-DATE
    > Task :cleanTest
    > Task :compileTestJava NO-SOURCE
    > Task :compileTestGroovy UP-TO-DATE
    > Task :processTestResources NO-SOURCE
    > Task :testClasses UP-TO-DATE
    
    > Task :test
    ToAlphanumericTest: testCall: SUCCESS
    Tests: 1, Failures: 0, Errors: 0, Skipped: 0
    
    BUILD SUCCESSFUL in 5s
    4 actionable tasks: 2 executed, 2 up-to-date

    So we can skip those tasks:

    > gw test --console verbose --exclude-task compileJava -x compileTestJava -x processResources -x processTestResources
    > Task :compileGroovy UP-TO-DATE
    > Task :classes
    > Task :cleanTest
    > Task :compileTestGroovy UP-TO-DATE
    > Task :testClasses
    
    > Task :test
    ToAlphanumericTest: testCall: SUCCESS
    Tests: 1, Failures: 0, Errors: 0, Skipped: 0
    
    BUILD SUCCESSFUL in 5s
    4 actionable tasks: 2 executed, 2 up-to-date  

    It doesn't seem to make a difference when there's only one test, but perhaps with a real test suite it would. In any case, I prefer to skip tests that will likely never have any code.

  • I found that you can set that in the build.gradle:

    gradle.startParameter.excludedTaskNames += ['compileJava', 'compileTestJava', 'processResources', 'processTestResources']

    Let's prove it:

    > gw test --console verbose
    > Task :compileGroovy UP-TO-DATE
    > Task :classes
    > Task :cleanTest
    > Task :compileTestGroovy UP-TO-DATE
    > Task :testClasses
    
    > Task :test
    ToAlphanumericTest: testCall: SUCCESS
    Tests: 1, Failures: 0, Errors: 0, Skipped: 0
    
    BUILD SUCCESSFUL in 4s
    4 actionable tasks: 2 executed, 2 up-to-date
  • We can also set the default task in the build.gradle:

    defaultTasks 'test'
  • Putting it all together, we can now just run gw to build and run the tests.

    > gw
    
    > Task :test
    ToAlphanumericTest: testCall: SUCCESS
    Tests: 1, Failures: 0, Errors: 0, Skipped: 0
    
    BUILD SUCCESSFUL in 4s
    4 actionable tasks: 2 executed, 2 up-to-date

See build.gradle for the complete file.

Mocking

Other than executing the code under test, Mocking is the most important part of unit testing. It lets you stub out external or unneeded parts of the code and focus on the logic you want to make sure is working.

This also wasn't working out of the box from the tutorial. The mock doesn't seem to be working, as the call to pwsh is still looking for a file Get-Repo.ps1:

void testCall() {
  // set up mocks
  def reponame = 'myRepoName'
  helper.registerAllowedMethod("pwsh", [Map.class], { reponame })

  // call getRepo and check result
  def result = getRepo()
  assert 'myRepoName'.equals(result)
}
> gw

> Task :test
GetRepoTest: testCall: FAILURE
> There were failing tests. See the report at: file:///C:/Users/mcascone1/Documents/github/test-jenk-unit/build/reports/tests/test/index.html

in the report:

groovy.lang.GroovyRuntimeException: Library Resource not found with path Get-Repo.ps1

I opened this question on StackOverflow. This is where I'm stuck right now.

Appendix

  1. The build/test results are output to a file: path/to/repo/build/reports/tests/test/index.html

  2. These old-school assert options might come in handy.

  3. Pro Tip: To run a single test class, or even a single test, run Gradle with the --tests option:

    ./gradlew test --tests GetRepoTest
    ./gradlew test --tests GetRepoTest.testCall
  4. I should probably move to the Spock framework ASAP. I don't think JPU and Spock are mutually-exclusive.