Testing Mule SMTP using MUnit MailServer Mock

Very often we use Mule SMTP connector in flows. How to ensure it is sending emails as expected? Let's write a test case for it.

Manik MagarManik Magar

We often use Mule SMTP connector in flows. But how to make sure that emails gets sent as expected?

MUnit provides MailServer that we can use to mock real SMTP server and verify the mails sent by SMTP connector. Lets see how we can write munit test case for below subflow:

<context:property-placeholder location="munit-mailserver-demo.properties" />

    <sub-flow name="subflow-mail-sender">
        <set-variable variableName="emailSubject" value="#['Welcome to UT']" doc:name="Set Subject"/>
        <smtp:outbound-endpoint host="${email.host}" port="${email.port}" to="${email.to.addr}" subject="#[flowVars.emailSubject]" cc="${email.cc.addr}" responseTimeout="10000" doc:name="SMTP" password="${email.password}" user="${email.user}"/>
    </sub-flow>

Testing Simple outbound SMTP

Step 1: MUnit MailServer dependency

Add below maven dependencies to maven project:

<dependency>
  <groupId>com.mulesoft.munit.utils</groupId>
  <artifactId>munit-mailserver-module</artifactId>
  <version>1.1.0</version>
  <scope>test</scope>
</dependency>

<!-- We will be using assertj for assertions -->
<dependency>
  <groupId>org.assertj</groupId>
  <artifactId>assertj-core</artifactId>
  <!-- use 2.8.0 for Java 7 projects -->
  <version>3.8.0</version>
  <scope>test</scope>
</dependency>

Step 2: Initialising MailServer in Test Case

Add a class level property of mailServer. We will use JUnit's @Before and @After methods to start and stop the mail server.

	private MailServer mailServer = new MailServer();

	@Override
	protected String getConfigResources() {
		return "munit-mailserver-demo.xml";
	}

	@Before
	public void setUp(){
		mailServer.start();
	}

	@After
	public void tearDown(){
		mailServer.stop();
	}

Step 3: Update Properties files for local SMTP settings

Our actual flow uses an external properties file to load the placeholders like email.host, email.port, email.to.addr and email.cc.addr. Make a copy of munit-mailserver-demo.properties under src\test\resource.

MailServer uses GreenMail library to implement the SMTP server. It uses GreenMail's ServerSetupTest configuration for setting up ports. So we will be using port 3025 for listening to the incoming mails.

override the properties in test file as below -

[email protected]
[email protected]

email.host=localhost
email.port=3025
email.user=
email.password=

Step 4: Write a test case

Now let's write a java test case.

@Test
	public void shouldSendEmail() throws Exception{
		String payload = "Welcome to UnitTesters.com";

		MuleEvent muleEvent = testEvent(payload);

		runFlow("subflow-mail-sender", muleEvent);

		List<MimeMessage> mails = mailServer.getReceivedMessages();

		assertThat(mails)
			.isNotEmpty()
			.hasSize(2);

		assertThat(mails)
			.allSatisfy(mimeMessage -> {
						// Let's inspect our message
						try {
							String toAddr = MailUtils.mailAddressesToString(mimeMessage.getRecipients(RecipientType.TO));
							String ccAddr = MailUtils.mailAddressesToString(mimeMessage.getRecipients(RecipientType.CC));

							assertThat(toAddr).as("To Address").isEqualTo("[email protected]");
							assertThat(ccAddr).as("CC Address").isEqualTo("[email protected]");
							assertThat(mimeMessage.getSubject()).as("Subject").isEqualTo("Welcome to UT");
							assertThat(mimeMessage.getContent().toString().trim()).as("Mail Body").isEqualTo(payload);

						} catch (MessagingException | IOException e) {
							fail("Unable to fetch data from Mail", e);
						}
					}
				);

	}

This is what test case does:

  1. Call the flow under test with a test event.
  2. Get the list of mails received on our test email server. We will receive one email per email address in to and cc addresses. So we should be having 2 emails on the server. We verify that with assertions.
  3. Next, using assertj's fluent assertions , we will iterate through each message received and perform assertions to verify recipients, subject, mail body.

This way, now we know what to expect from the SMTP component.

Testing Mails with Attachment

Using the similar techniques and assertj, we can verify the attachments on outbound emails. Let's consider below flow for testing -

<sub-flow name="subflow-attachment-mail-sender">
        <set-variable variableName="emailSubject" value="#['Welcome to UT']" doc:name="Set Subject"/>

        <set-attachment attachmentName="test.txt" value="#[payload]" contentType="text/plain" doc:name="Attachment" />

        <smtp:outbound-endpoint host="${email.host}" port="${email.port}" to="${email.to.addr}" subject="#[flowVars.emailSubject]" cc="${email.cc.addr}" responseTimeout="10000" doc:name="SMTP" password="${email.password}" user="${email.user}"/>
    </sub-flow>

Test case for this could look like below -

@Test
	public void shouldSendEmailWithAttachment() throws Exception{
		String payload = "Welcome to UnitTesters.com";

		MuleEvent muleEvent = testEvent(payload);

		runFlow("subflow-attachment-mail-sender", muleEvent);

		List<MimeMessage> mails = mailServer.getReceivedMessages();

		assertThat(mails)
			.isNotEmpty()
			.hasSize(2);

		assertThat(mails)
			.allSatisfy(mimeMessage -> {
						// Verify the attachment
						try {

							MimeBodyPart attachment = getAttachments(mimeMessage, "test.txt");
							assertThat(attachment).isNotNull();
							assertThat(attachment.getContentType()).startsWith("text/plain");
							assertThat(attachment.getContent().toString().trim()).isEqualTo(payload);

							//Just to test non-existent attachment.
							MimeBodyPart attachment2 = getAttachments(mimeMessage, "test2.txt");
							assertThat(attachment2).isNull();

						} catch (MessagingException | IOException e) {
							fail("Unable to fetch data from Mail", e);
						}
					}
				);

	}

	public MimeBodyPart getAttachments(MimeMessage msg, String attchmentName) throws MessagingException, IOException{
		Map<String, Part> attachments = new HashMap<String, Part>();
		MailUtils.getAttachments((Multipart)msg.getContent(), attachments);

		return (MimeBodyPart) attachments.get(attchmentName);

	}

This is what test case does:

  1. Call the flow under test with a test event.
  2. Get the list of mails received on our test email server. Again we should recieve two emails on the server.
  3. Next, using assertj's fluent assertions , we extract the attachments and verify their presence, content, mime type etc.

That was easy, right! Hope this helps you to test your SMTP flows in Mule.

Source code

Full source of this demo is available on Github here. Feel free to download and take a look.

Refernces:

‚Äč

Feel free to let me know your thoughts in comments and you can follow me on twitter @manikmagar or @UnitTesters.

Unit Testing DataWeave JSON output

Often we generate JSON using DataWeave. What about testing it?

Manik MagarManik Magar

In the previous post about unit testing DataWeave scripts with MUnit and JUnit, I showed you how to verify Java and CSV output of DataWeave scripts. We also looked at some error troubleshooting in dataweave scripts.

Now, lets look at DataWeave with JSON output and how we can test the content of our output with MUnit and JUnit.

DataWeave Script

Let's use the same DataWeave script from our previous post and change the output type to application/json.

%dw 1.0
%output application/json
---
payload.root.*employee map {

		name: $.fname ++ ' ' ++ $.lname,
		dob: $.dob,
		age: (now as :string {format: "yyyy"}) -  
				(($.dob as :date {format:"MM-dd-yyyy"}) as :string {format:"yyyy"})

}

Input XML Payload:

<?xml version='1.0' encoding='UTF-8'?>
<root>
	<employee>
		<fname>M1</fname>
		<lname>M2</lname>
		<dob>01-01-1980</dob>
	</employee>
	<employee>
		<fname>A1</fname>
		<lname>A2</lname>
		<dob>12-23-1995</dob>
	</employee>
</root>

Expected output:

[
  {
    "name": "M1 M2",
    "dob": "01-01-1980",
    "age": 36
  },
  {
    "name": "A1 A2",
    "dob": "12-23-1995",
    "age": 21
  }
]

Writing MUnit XML Test Case

As we saw in previous post, output of DataWeave will be instance of WeaveOutputHandler class. Any transformer capable of consuming output streams can consume this output. As we are expecting json output, we will use json-to-object-transformer with a return class of java.util.ArrayList. Once we have the java list of json, we can validate any data elements. Here is out xml test case -

    <munit:test name="dataweave-testing-suite-jsonTest" description="MUnit Test">
        <munit:set payload="#[getResource('sample_data/employees.xml').asStream()]" mimeType="application/xml" doc:name="Set Message"/>
        <flow-ref name="dataweave-testingSub_Flow" doc:name="dataweave-testingSub_Flow"/>
        <json:json-to-object-transformer returnClass="java.util.ArrayList" doc:name="JSON to Object"/>
        <munit:assert-on-equals expectedValue="#[2]" actualValue="#[payload.size()]" doc:name="Assert Equals"/>
        <munit:assert-on-equals expectedValue="#[36]" actualValue="#[payload[0].age]" doc:name="Assert Equals"/>
    </munit:test>

Note: It may also be possible to convert the DataWeave output to JSON string and then use JSON evaluator for MEL Expression.

Writing Java JUnit Test Case

We can also use java to write our test case. Logic and steps will be similar to that of xml. Here is our java test case -

@Test
	public void testJsonOutput() throws Exception {
		String payload = FileUtils.readFileToString(
				new File(DataWeaveTests.class.getClassLoader().getResource("sample_data/employees.xml").getPath()));

		MuleEvent event = testEvent(payload);
		((DefaultMuleMessage) event.getMessage()).setMimeType(MimeTypes.APPLICATION_XML);

		MuleEvent reply = runFlow("dataweave-testingSub_Flow", event);

		//Create and initialise JSON to Object transformer. All below steps are required.
		JsonToObject jto = new JsonToObject();
		jto.setMuleContext(muleContext);
		jto.setReturnDataType(DataTypeFactory.create(ArrayList.class, HashMap.class));
		jto.initialise();


		List<Map> data = (List<Map>) jto.transform(reply.getMessage().getPayloadAsString(), reply);

		Assert.assertEquals(2, data.size());
		Assert.assertEquals(36, data.get(0).get("age"));
	}

Note: If you run into some error like "more than one transformers found" for getPayloadAsString() method, then try using ObjectToString transformer to convert to dataweave output to String.

ObjectToString ots = new ObjectToString();
		ots.setMuleContext(muleContext);
		ots.initialise();
		List<Map> data = (List<Map>) jto.transform(ots.transform(reply.getMessage().getPayload()), reply);

And That's all about it, so simple :)

Hope this helps to write safe code!

Overriding Properties in MUnit XML and Java for testing

It is very common for any mule application to use external properties files. In this post, we will see how we can override properties values for testing.

Manik MagarManik Magar

It is very common for any mule application to use external properties files. In this post, we will see how we can override properties values for testing. We will also cover how we can write to temporary folder during munit test, disable connector mocking and asserting file existence.

For demonstration purpose, we will have a flow that uses DataWeave to convert xml file into csv and writes to an output folder using file:outbound-endpoint. Let's read the output path from properties file with key explore.mule.target.folder. As we are going to write file during testing, we will need munit not to mock the connectors.

Here is our production code that declares a context:property-placeholder to read properties from src/main/resources/explore-mule.properties

<?xml version="1.0" encoding="UTF-8"?>

<mule xmlns:context="http://www.springframework.org/schema/context"
	xmlns:dw="http://www.mulesoft.org/schema/mule/ee/dw"
	xmlns:file="http://www.mulesoft.org/schema/mule/file"
	xmlns="http://www.mulesoft.org/schema/mule/core" xmlns:doc="http://www.mulesoft.org/schema/mule/documentation"
	xmlns:spring="http://www.springframework.org/schema/beans" version="EE-3.8.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-current.xsd
http://www.mulesoft.org/schema/mule/ee/dw http://www.mulesoft.org/schema/mule/ee/dw/current/dw.xsd
http://www.mulesoft.org/schema/mule/file http://www.mulesoft.org/schema/mule/file/current/mule-file.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-current.xsd
http://www.mulesoft.org/schema/mule/core http://www.mulesoft.org/schema/mule/core/current/mule.xsd">

    <context:property-placeholder location="explore-mule.properties"/>


    <flow name="properties-testingFlow3">
        <file:inbound-endpoint path="input3" moveToDirectory="output" responseTimeout="10000" doc:name="File"/>
        <dw:transform-message doc:name="Transform Message">
            <dw:input-payload doc:sample="sample_data/empty.xml"/>
            <dw:set-payload resource="classpath:dwl/employees2.dwl"/>
        </dw:transform-message>
        <file:outbound-endpoint path="${explore.mule.target.folder}" outputPattern="output.csv" doc:name="File"/>
    </flow>

</mule>

Overriding Properties in XML

Similar to specifying properties in production code, we can create a copy of properties files under /src/test/resource/env/test/explore-mule.properties and refer to it inside xml using context:property-placeholder.

Below XML MUnit suite shows this option in action. In test explore-mule.properties, we set explore.mule.target.folder=test-output and then verify that output.csv exists after test case executes.

<?xml version="1.0" encoding="UTF-8"?>

<mule xmlns:context="http://www.springframework.org/schema/context"
	xmlns="http://www.mulesoft.org/schema/mule/core" xmlns:doc="http://www.mulesoft.org/schema/mule/documentation" xmlns:munit="http://www.mulesoft.org/schema/mule/munit" xmlns:spring="http://www.springframework.org/schema/beans" xmlns:core="http://www.mulesoft.org/schema/mule/core" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-current.xsd
http://www.mulesoft.org/schema/mule/munit http://www.mulesoft.org/schema/mule/munit/current/mule-munit.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-current.xsd
http://www.mulesoft.org/schema/mule/core http://www.mulesoft.org/schema/mule/core/current/mule.xsd">

 <context:property-placeholder location="env/test/explore-mule.properties"/>

    <munit:config name="munit" doc:name="MUnit configuration" mock-connectors="false"/>
    <spring:beans>
        <spring:import resource="classpath:sample-flows.xml"/>
    </spring:beans>
    <munit:test name="sample-flows-test-suite-properties-testingFlow3Test" description="Test">
    	<munit:set
			payload="#[getResource('sample_data/employees.xml').asStream()]"
			doc:name="Set Message" mimeType="application/xml" />
        <flow-ref name="properties-testingFlow3" doc:name="Flow-ref to properties-testingFlow3"/>
        <munit:assert-true condition="#[new java.io.File('./test-output/output.csv').exists()]" doc:name="Assert True"/>

    </munit:test>
</mule>

Don't forget to disable connector mocking by adding mock-connectors="false" in munit:config.

Overriding Properties in FunctionalMunitSuite

When you write a munit test case in java using FunctionalMunitSuite, it is more flexible to set properties. When FunctionalMunitSuite creates the mocking configuration during init, it calls a protected method protected Properties getStartUpProperties() to get the properties for tests. Default implementation in FunctionalMUnitSuite returns null but we can easily override this function in our test suite to return an instance of java.util.Properties.

One benefit of this over xml approach is, you get to use power of java while setting properties values. In this example, we will use org.junit.rules.TemporaryFolder to create a temporary folder and set that as a target folder. If we really use this as a junit Rule then JUnit can take care of deleting temporary folder, but here we can use that as rule because getStartUpProperties is called once a testSuite/context initialization so we will keep reference to our properties and folders. So we will also add an AfterClass method to delete this folder.

Below Java code shows this in action. At the end of our test case, we assert that the target temporary folder contains output.csv.

package com.mms.mule.explore;

import java.io.File;
import java.io.IOException;
import java.util.Properties;

import org.hamcrest.MatcherAssert;
import org.hamcrest.Matchers;
import org.junit.AfterClass;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.mule.DefaultMuleMessage;
import org.mule.api.MuleEvent;
import org.mule.munit.runner.functional.FunctionalMunitSuite;
import org.mule.transformer.types.MimeTypes;
import org.mule.util.FileUtils;

public class PropertiesTestSuite extends FunctionalMunitSuite {

	private Properties props;
	private static TemporaryFolder tempFolder;

	@Override
	protected String getConfigResources() {
		return "sample-flows.xml";
	}

	@Override
	protected boolean haveToMockMuleConnectors() {
		return false;
	}

	@AfterClass
	public static void cleanup(){
		tempFolder.delete();
	}

	@Override
	protected Properties getStartUpProperties() {
		props = super.getStartUpProperties();
		if(props == null){
			props = new Properties();
		}
		tempFolder = new TemporaryFolder();
		try {
			tempFolder.create();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		String path =tempFolder.getRoot().getAbsolutePath();
		System.out.println("Setting path to - "+ path);
		props.setProperty("explore.mule.target.folder", path);

		return props;
	}

	@Test
	public void testFileWriting() throws Exception{
		String payload = FileUtils.readFileToString(new File(DataWeaveTests.class.getClassLoader().getResource("sample_data/employees.xml").getPath()));

		MuleEvent event = testEvent(payload);
		((DefaultMuleMessage)event.getMessage()).setMimeType(MimeTypes.APPLICATION_XML);

		MuleEvent reply = runFlow("properties-testingFlow3", event);

		MatcherAssert.assertThat(new File(tempFolder.getRoot(), "output.csv").exists(),Matchers.equalTo(Boolean.TRUE));
	}
}

Don't forget to override haveToMockMuleConnectors() and return false to allow file writing.

As an alternative to overriding getStartUpProperties method, you can also create a sample munit xml config with context:properties-placeholder and then use that inside getConfigResources() method.

Test Application Source

Test Application source code is available on Github here.

Conclusion

MUnit provides a very stable environment for testing mule flows. You can easily override your production properties inside MUnit XML as well as Java test suite.