nooks and crannies: lessons from fhir server testing · 2020. 8. 30. · 4 fhir server...
TRANSCRIPT
HL7®, FHIR® and the flame Design mark are the registered trademarks of Health Level Seven International and are used with permission.
HL7 FHIR DevDays 2020, Virtual Edition US, June 15–18, 2020 | @HL7 @FirelyTeam | #fhirdevdays | www.devdays.com/us
Nooks and Crannies: Lessons from FHIR Server Testing
Lee Surprenant, IBM
1
Traditional testing pyramid
source:https://james-willett.com/2016/09/the-evolution-of-the-testing-pyramid/
2
Our focus today(but applicable to all levels)
Community Test Tools• AEGIS Touchstone• Inferno
3
FHIR Server components
serialization/deserialization of resources (JSON and XML)
schema (primary), constraints (secondary), and terminology
storing and retrieving resources
1. extraction / indexing2. search query
FHIR Model
Validation
Search
Persistence
interceptors, extended operations, etc.Logic
4
FHIR Server example-driven component tests
FHIR Model
Validation
Search
Persistence
Logic
Round-trip resources from files to objects and back
Positive and negative example validation (core spec and profiles / implementation guides)
Simulate extraction of search parameters and search on each parameter defined in the base spec
Round-trip each resource to the database
Test against all variations of resources; look for NullPointerException and similar logic errors
5
Sources for test data
• Core specification• Synthea• Implementation-guide specific /
Connectathon resources• http://hl7.org/fhir/us/carin-
bb/2020Feb/Examples.html• https://github.com/HL7-
DaVinci/pdex-plan-net-sample-data• …
• IBM-generated examples
6
com.ibm.fhir:fhir-examples
• Java module that packages many examples in a single jar• 2 Classes on top:• com.ibm.fhir.examples.ExamplesUtil• com.ibm.fhir.examples.Index
7
IBM-generated examples
Three flavors:1. minimal2. complete-mock3. complete-absent
Also “basic-search”examples (time permitting)
8
Hypothetical…private static void printActivePatientSurnames(List<Patient> patients) {
for (Patient patient : patients) {
if (patient.getActive().getValue()) {
for (HumanName name : patient.getName()) {
if (name.getFamily() != null) {
System.out.println(name.getFamily().getValue());
}
}
}
}
}
9
Minimal (Patient){
"resourceType": "Patient",
"meta": {
"tag": [
{
"code": "ibm/minimal"
}
]
}
}
json/ibm/minimal/Patient-1.json
10
Minimal (Patient){
"resourceType": "Patient",
"meta": {
"tag": [
{
"code": "ibm/minimal"
}
]
}
}
json/ibm/minimal/Patient-1.json
private static void printActivePatientSurnames(List<Patient> patients) {
for (Patient patient : patients) {
if (patient.getActive().getValue()) {
for (HumanName name : patient.getName()) {
if (name.getFamily() != null) {
System.out.println(name.getFamily().getValue());
}
}
}
}
}
11
NullPointerException
12
Null checkingprivate static void printActivePatientSurnames(List<Patient> patients) {
for (Patient patient : patients) {
if (patient.getActive().getValue()) {
for (HumanName name : patient.getName()) {
if (name.getFamily() != null) {
System.out.println(name.getFamily().getValue());
}
}
}
}
}
13
Null checkingprivate static void printActivePatientSurnames(List<Patient> patients) {
for (Patient patient : patients) {
if (patient.getActive() != null && patient.getActive().getValue()) {
for (HumanName name : patient.getName()) {
if (name.getFamily() != null) {
System.out.println(name.getFamily().getValue());
}
}
}
}
}
14
Minimal (Observation){
"resourceType": "Observation",
"meta": …
"_status": {
"extension": [{
"url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason",
"valueCode": "unknown"
}]
},
"code": {
"extension": [{
"url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason",
"valueCode": "unknown"
}]
} json/ibm/minimal/Observation-1.json
15
Complete-mock{
"resourceType": "Patient",
"meta": …
"identifier": [
{
"use": "official",
"type": {
"coding": [
{
"system": "z6qLT3jgB9",
"version": "aZWQtezZjY",
"code": "7J8uWBfh5a",
"display": "TFEvRt3UO0",
"userSelected": true
… json/ibm/complete-mock/Patient-1.json
16
Complete-mock{
"resourceType": "Patient",
"meta": …
"identifier": [
{
"use": "official",
"type": {
"coding": [
{
"system": "z6qLT3jgB9",
"version": "aZWQtezZjY",
"code": "7J8uWBfh5a",
"display": "TFEvRt3UO0",
"userSelected": true
…
17
Complete-mock codes and choice types{
"resourceType": "Patient",
"meta": …
"identifier": …
"active": true,
"gender": "other",
"deceasedBoolean": true,
…
{
"resourceType": "Patient",
"meta": …
"identifier": …
"active": true,
"gender": "unknown",
"deceasedDateTime": "2020-03-12T00:11:56.829-04:00",
…
json/ibm/complete-mock/Patient-1.json json/ibm/complete-mock/Patient-2.json
18
Complete-absent{
"resourceType": "Patient",
"meta": …
"identifier": [{
"_use": {
"extension": [{
"url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason",
"valueCode": "unknown"
}]
}, …
"_active": {
"extension": [{
"url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason",
"valueCode": "unknown"
}]
},json/ibm/complete-absent/Patient-1.json
19
Complete-absent{
"resourceType": "Patient",
"meta": …
"identifier": [{
"_use": {
"extension": [{
"url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason",
"valueCode": "unknown"
}]
}, …
"_active": {
"extension": [{
"url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason",
"valueCode": "unknown"
}]
},json/ibm/complete-absent/Patient-1.json
private static void printActivePatientSurnames(List<Patient> patients) {
for (Patient patient : patients) {
if (patient.getActive() != null && patient.getActive().getValue()) {
for (HumanName name : patient.getName()) {
if (name.getFamily() != null) {
System.out.println(name.getFamily().getValue());
}
}
}
}
}
20
NullPointerException
21
Primitive extensions (XML)<Patient xmlns="http://hl7.org/fhir">
<meta> …
<name>
<family>
<extensionurl="http://hl7.org/fhir/StructureDefinition/data-absent-reason">
<valueCode value="unknown"/>
</extension>
</family>
<given>
<extensionurl="http://hl7.org/fhir/StructureDefinition/data-absent-reason">
<valueCode value="unknown"/>
</extension>
</given>
…
<Patient xmlns="http://hl7.org/fhir">
<meta> …
<name>
<family value="Donald"/>
<given value="Duck"/>
<given value="D"/>
</name>
…
22
Primitive extensions (JSON){
"resourceType": "Patient",
"meta": …
"name": [{
"_family": {
"extension": [{
"url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason",
"valueCode": "unknown"
}]
},
"given": [null],
"_given": [{
"extension": [{
"url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason",
"valueCode": "unknown"
}]
{
"resourceType": "Patient",
"meta": …
"name": [{
"family": "Donald",
"given": [
"Duck",
"D"
]
}],
…
23
Null checkingprivate static void printActivePatientSurnames(List<Patient> patients) {
for (Patient patient : patients) {
if (patient.getActive() != null && patient.getActive().getValue()) {
for (HumanName name : patient.getName()) {
if (name.getFamily() != null) {
System.out.println(name.getFamily().getValue());
}
}
}
}
}
24
Null checkingprivate static void printActivePatientSurnames(List<Patient> patients) {
for (Patient patient : patients) {
if (patient.getActive() != null && Boolean.TRUE.equals(patient.getActive().getValue())) {
for (HumanName name : patient.getName()) {
if (name.getFamily() != null) {
System.out.println(name.getFamily().getValue());
}
}
}
}
}
26
Questions?
27
Bonus: IBM fhir-persistence search tests
• Organized by search parameter type• Covers all allowed type mappings defined at
https://www.hl7.org/fhir/search.html#table• Basic resources with extensions (1 or more per data type)• Custom SearchParameter definitions (>1 for complex types)
28