Skip to content
This repository has been archived by the owner on Dec 13, 2023. It is now read-only.

Commit

Permalink
Merge pull request #3507 from v1r3n/testing_framework
Browse files Browse the repository at this point in the history
Support for unit and integration tests of the workflows
  • Loading branch information
v1r3n authored Mar 13, 2023
2 parents 445a05c + 6d6d1c1 commit a4500f1
Show file tree
Hide file tree
Showing 20 changed files with 1,724 additions and 6 deletions.
2 changes: 2 additions & 0 deletions client/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ dependencies {

implementation project(':conductor-common')
implementation "com.sun.jersey:jersey-client:${revJersey}"
implementation "javax.ws.rs:javax.ws.rs-api:${revJAXRS}"
implementation "org.glassfish.jersey.core:jersey-common:${revJerseyCommon}"

implementation "com.netflix.spectator:spectator-api:${revSpectator}"
implementation ("com.netflix.eureka:eureka-client:${revEurekaClient}") {
Expand Down
24 changes: 24 additions & 0 deletions client/dependencies.lock
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
"commons-io:commons-io": {
"locked": "2.7"
},
"javax.ws.rs:javax.ws.rs-api": {
"locked": "2.1.1"
},
"org.apache.commons:commons-lang3": {
"locked": "3.12.0"
},
Expand All @@ -47,6 +50,9 @@
"org.apache.logging.log4j:log4j-web": {
"locked": "2.17.2"
},
"org.glassfish.jersey.core:jersey-common": {
"locked": "2.22.2"
},
"org.jetbrains:annotations": {
"locked": "23.0.0"
},
Expand Down Expand Up @@ -109,6 +115,9 @@
"commons-io:commons-io": {
"locked": "2.7"
},
"javax.ws.rs:javax.ws.rs-api": {
"locked": "2.1.1"
},
"org.apache.bval:bval-jsr": {
"firstLevelTransitive": [
"com.netflix.conductor:conductor-common"
Expand Down Expand Up @@ -156,6 +165,9 @@
],
"locked": "2.17.2"
},
"org.glassfish.jersey.core:jersey-common": {
"locked": "2.22.2"
},
"org.slf4j:slf4j-api": {
"locked": "1.7.36"
}
Expand Down Expand Up @@ -185,6 +197,9 @@
"commons-io:commons-io": {
"locked": "2.7"
},
"javax.ws.rs:javax.ws.rs-api": {
"locked": "2.1.1"
},
"junit:junit": {
"locked": "4.13.2"
},
Expand All @@ -209,6 +224,9 @@
"org.codehaus.groovy:groovy-all": {
"locked": "2.5.13"
},
"org.glassfish.jersey.core:jersey-common": {
"locked": "2.22.2"
},
"org.junit.vintage:junit-vintage-engine": {
"locked": "5.8.2"
},
Expand Down Expand Up @@ -289,6 +307,9 @@
"commons-io:commons-io": {
"locked": "2.7"
},
"javax.ws.rs:javax.ws.rs-api": {
"locked": "2.1.1"
},
"junit:junit": {
"locked": "4.13.2"
},
Expand Down Expand Up @@ -342,6 +363,9 @@
"org.codehaus.groovy:groovy-all": {
"locked": "2.5.13"
},
"org.glassfish.jersey.core:jersey-common": {
"locked": "2.22.2"
},
"org.junit.vintage:junit-vintage-engine": {
"locked": "5.8.2"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import com.netflix.conductor.common.run.SearchResult;
import com.netflix.conductor.common.run.Workflow;
import com.netflix.conductor.common.run.WorkflowSummary;
import com.netflix.conductor.common.run.WorkflowTestRequest;
import com.netflix.conductor.common.utils.ExternalPayloadStorage;

import com.sun.jersey.api.client.ClientHandler;
Expand Down Expand Up @@ -492,4 +493,13 @@ public SearchResult<Workflow> searchV2(
};
return getForEntity("workflow/search-v2", params, searchResultWorkflow);
}

public Workflow testWorkflow(WorkflowTestRequest testRequest) {
Validate.notNull(testRequest, "testRequest cannot be null");
if (testRequest.getWorkflowDef() != null) {
testRequest.setName(testRequest.getWorkflowDef().getName());
testRequest.setVersion(testRequest.getWorkflowDef().getVersion());
}
return postForEntity("workflow/test", testRequest, null, Workflow.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
* Copyright 2023 Netflix, Inc.
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package com.netflix.conductor.client.testing;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.TestInstance;

import com.netflix.conductor.client.http.MetadataClient;
import com.netflix.conductor.client.http.WorkflowClient;
import com.netflix.conductor.common.config.ObjectMapperProvider;
import com.netflix.conductor.common.metadata.tasks.TaskResult;
import com.netflix.conductor.common.metadata.tasks.TaskType;
import com.netflix.conductor.common.metadata.workflow.WorkflowDef;
import com.netflix.conductor.common.metadata.workflow.WorkflowTask;
import com.netflix.conductor.common.run.Workflow;
import com.netflix.conductor.common.run.WorkflowTestRequest;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

import static org.junit.jupiter.api.Assertions.assertNotNull;

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public abstract class AbstractWorkflowTests {

protected static ObjectMapper objectMapper = new ObjectMapperProvider().getObjectMapper();

protected static TypeReference<Map<String, List<WorkflowTestRequest.TaskMock>>> mockType =
new TypeReference<Map<String, List<WorkflowTestRequest.TaskMock>>>() {};

protected MetadataClient metadataClient;

protected WorkflowClient workflowClient;

@BeforeAll
public void setup() {

String baseURL = "http://localhost:8080/api/";
metadataClient = new MetadataClient();
metadataClient.setRootURI(baseURL);

workflowClient = new WorkflowClient();
workflowClient.setRootURI(baseURL);
}

protected WorkflowTestRequest getWorkflowTestRequest(WorkflowDef def) throws IOException {
WorkflowTestRequest testRequest = new WorkflowTestRequest();
testRequest.setInput(new HashMap<>());
testRequest.setName(def.getName());
testRequest.setVersion(def.getVersion());
testRequest.setWorkflowDef(def);

Map<String, List<WorkflowTestRequest.TaskMock>> taskRefToMockOutput = new HashMap<>();
for (WorkflowTask task : def.collectTasks()) {
List<WorkflowTestRequest.TaskMock> taskRuns = new LinkedList<>();
WorkflowTestRequest.TaskMock mock = new WorkflowTestRequest.TaskMock();
mock.setStatus(TaskResult.Status.COMPLETED);
Map<String, Object> output = new HashMap<>();

output.put("response", Map.of());
mock.setOutput(output);
taskRuns.add(mock);
taskRefToMockOutput.put(task.getTaskReferenceName(), taskRuns);

if (task.getType().equals(TaskType.SUB_WORKFLOW.name())) {
Object inlineSubWorkflowDefObj = task.getSubWorkflowParam().getWorkflowDefinition();
if (inlineSubWorkflowDefObj != null) {
// If not null, it represents WorkflowDef object
WorkflowDef inlineSubWorkflowDef = (WorkflowDef) inlineSubWorkflowDefObj;
WorkflowTestRequest subWorkflowTestRequest =
getWorkflowTestRequest(inlineSubWorkflowDef);
testRequest
.getSubWorkflowTestRequest()
.put(task.getTaskReferenceName(), subWorkflowTestRequest);
} else {
// Inline definition is null
String subWorkflowName = task.getSubWorkflowParam().getName();
// Load up the sub workflow from the JSON
WorkflowDef subWorkflowDef =
getWorkflowDef("/workflows/" + subWorkflowName + ".json");
assertNotNull(subWorkflowDef);
WorkflowTestRequest subWorkflowTestRequest =
getWorkflowTestRequest(subWorkflowDef);
testRequest
.getSubWorkflowTestRequest()
.put(task.getTaskReferenceName(), subWorkflowTestRequest);
}
}
}
testRequest.setTaskRefToMockOutput(taskRefToMockOutput);
return testRequest;
}

protected WorkflowDef getWorkflowDef(String path) throws IOException {
InputStream inputStream = AbstractWorkflowTests.class.getResourceAsStream(path);
if (inputStream == null) {
throw new IOException("No file found at " + path);
}
return objectMapper.readValue(new InputStreamReader(inputStream), WorkflowDef.class);
}

protected Workflow getWorkflow(String path) throws IOException {
InputStream inputStream = AbstractWorkflowTests.class.getResourceAsStream(path);
if (inputStream == null) {
throw new IOException("No file found at " + path);
}
return objectMapper.readValue(new InputStreamReader(inputStream), Workflow.class);
}

protected Map<String, List<WorkflowTestRequest.TaskMock>> getTestInputs(String path)
throws IOException {
InputStream inputStream = AbstractWorkflowTests.class.getResourceAsStream(path);
if (inputStream == null) {
throw new IOException("No file found at " + path);
}
return objectMapper.readValue(new InputStreamReader(inputStream), mockType);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2023 Netflix, Inc.
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package com.netflix.conductor.client.testing;

import java.math.BigDecimal;

public class LoanWorkflowInput {

private String userEmail;

private BigDecimal loanAmount;

public String getUserEmail() {
return userEmail;
}

public void setUserEmail(String userEmail) {
this.userEmail = userEmail;
}

public BigDecimal getLoanAmount() {
return loanAmount;
}

public void setLoanAmount(BigDecimal loanAmount) {
this.loanAmount = loanAmount;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* Copyright 2023 Netflix, Inc.
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package com.netflix.conductor.client.testing;

import java.io.IOException;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;

import com.netflix.conductor.common.metadata.tasks.Task;
import com.netflix.conductor.common.metadata.workflow.WorkflowDef;
import com.netflix.conductor.common.run.Workflow;
import com.netflix.conductor.common.run.WorkflowTestRequest;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

/** Unit test a workflow with inputs read from a file. */
public class LoanWorkflowTest extends AbstractWorkflowTests {

/** Uses mock inputs to verify the workflow execution and input/outputs of the tasks */
// Tests are commented out since it requires a running server
// @Test
public void verifyWorkflowExecutionWithMockInputs() throws IOException {
WorkflowDef def = getWorkflowDef("/workflows/calculate_loan_workflow.json");
assertNotNull(def);
Map<String, List<WorkflowTestRequest.TaskMock>> testInputs =
getTestInputs("/test_data/loan_workflow_input.json");
assertNotNull(testInputs);

WorkflowTestRequest testRequest = new WorkflowTestRequest();
testRequest.setWorkflowDef(def);

LoanWorkflowInput workflowInput = new LoanWorkflowInput();
workflowInput.setUserEmail("[email protected]");
workflowInput.setLoanAmount(new BigDecimal(11_000));
testRequest.setInput(objectMapper.convertValue(workflowInput, Map.class));

testRequest.setTaskRefToMockOutput(testInputs);
testRequest.setName(def.getName());
testRequest.setVersion(def.getVersion());

Workflow execution = workflowClient.testWorkflow(testRequest);
assertNotNull(execution);

// Assert that the workflow completed successfully
assertEquals(Workflow.WorkflowStatus.COMPLETED, execution.getStatus());

// Ensure the inputs were captured correctly
assertEquals(
workflowInput.getLoanAmount().toString(),
String.valueOf(execution.getInput().get("loanAmount")));
assertEquals(workflowInput.getUserEmail(), execution.getInput().get("userEmail"));

// A total of 3 tasks were executed
assertEquals(3, execution.getTasks().size());

Task fetchUserDetails = execution.getTasks().get(0);
Task getCreditScore = execution.getTasks().get(1);
Task calculateLoanAmount = execution.getTasks().get(2);

// fetch user details received the correct input from the workflow
assertEquals(
workflowInput.getUserEmail(), fetchUserDetails.getInputData().get("userEmail"));

// And that the task produced the right output
int userAccountNo = 12345;
assertEquals(userAccountNo, fetchUserDetails.getOutputData().get("userAccount"));

// get credit score received the right account number from the output of the fetch user
// details
assertEquals(userAccountNo, getCreditScore.getInputData().get("userAccountNumber"));
int expectedCreditRating = 750;

// The task produced the right output
assertEquals(expectedCreditRating, getCreditScore.getOutputData().get("creditRating"));

// Calculate loan amount gets the right loan amount from workflow input
assertEquals(
workflowInput.getLoanAmount().toString(),
String.valueOf(calculateLoanAmount.getInputData().get("loanAmount")));

// Calculate loan amount gets the right credit rating from the previous task
assertEquals(expectedCreditRating, calculateLoanAmount.getInputData().get("creditRating"));

int authorizedLoanAmount = 10_000;
assertEquals(
authorizedLoanAmount,
calculateLoanAmount.getOutputData().get("authorizedLoanAmount"));

// Finally, lets verify the workflow outputs
assertEquals(userAccountNo, execution.getOutput().get("accountNumber"));
assertEquals(expectedCreditRating, execution.getOutput().get("creditRating"));
assertEquals(authorizedLoanAmount, execution.getOutput().get("authorizedLoanAmount"));

System.out.println(execution);
}
}
Loading

0 comments on commit a4500f1

Please sign in to comment.