Overview

Sniffy is a Java profiler which shows the results directly in your browser. It also brings profiling to your unit (or rather component) tests and allows you to disable certain outgoing connections for fault-tolerance testing. It can even record network traffic sent via Java plain old Socket API as well as via NIO API.

In-browser profiler

demo

Asserting number of queries in unit tests

    @Rule public SniffyRule sniffyRule = new SniffyRule();

    @Rule public ExpectedException thrown = ExpectedException.none();

    @Test
    @SqlExpectation(count = @Count(1))
    public void testExpectedOneQueryGotOne() throws SQLException {
        DriverManager.getConnection("sniffy:jdbc:h2:mem:", "sa", "sa").createStatement().execute("SELECT 1 FROM DUAL"); // (4)
    }

    @Test
    @SqlExpectation(count = @Count(max = 1), query = SqlStatement.SELECT)
    public void testExpectedNotMoreThanOneSelectGotTwo() throws SQLException {
        try (Statement statement = DriverManager.getConnection("sniffy:jdbc:h2:mem:", "sa", "sa").createStatement()) {
            statement.execute("SELECT 1 FROM DUAL");
            statement.execute("SELECT 2 FROM DUAL");
        }
        thrown.expect(WrongNumberOfQueriesError.class);
    }

Testing bad connectivity

Discover all outgoing network connections from your server and disable them right from your browser:

network connections

Sniffy will throw a java.net.ConnectException when your application tries to connect to address disallowed by Sniffy. It’s also possible to specify a fixed delay for all sent and received TCP packets.

Simulating no connectivity in unit tests

    @Rule public SniffyRule sniffyRule = new SniffyRule();

    @Test
    @DisableSockets
    public void testDisableSockets() throws IOException {
        try {
            new Socket("google.com", 22);
            fail("Sniffy should have thrown ConnectException");
        } catch (ConnectException e) {
            assertNotNull(e);
        }
    }

Recording network traffic

    @Test
    public void testCaptureTraffic() throws Exception {

        try (Spy<?> spy = Sniffy.spy(
                SpyConfiguration.builder().captureNetworkTraffic(true).build()) // (1)
        ) {

            performSocketOperation(); // (2)

            Map<SocketMetaData, List<NetworkPacket>> networkTraffic = spy.getNetworkTraffic( // (3)
                    Threads.ANY, // (4)
                    AddressMatchers.anyAddressMatcher(), // (5)
                    GroupingOptions.builder().
                            groupByThread(false). // (6)
                            groupByStackTrace(false). // (7)
                            groupByConnection(false). // (8)
                            build()
            );

            assertEquals(1, networkTraffic.size());

            for (Map.Entry<SocketMetaData, List<NetworkPacket>> entry : networkTraffic.entrySet()) {

                SocketMetaData socketMetaData = entry.getKey(); // (9)

                Protocol protocol = socketMetaData.getProtocol(); // say TCP
                String hostName = socketMetaData.getAddress().getHostName(); // say "hostname.acme.com"
                int port = socketMetaData.getAddress().getPort(); // say 443

                List<NetworkPacket> networkPackets = entry.getValue(); // (10)

                assertArrayEquals(REQUEST, networkPackets.get(0).getBytes());
                assertTrue(networkPackets.get(0).isSent());

                assertArrayEquals(RESPONSE, networkPackets.get(1).getBytes());
                assertFalse(networkPackets.get(1).isSent());

            }

        }

    }

Install

Standalone setup

Sniffy comes with an uber-jar which doesn’t require any additional dependencies to be installed. Just grab the sniffy-3.1.12.jar from our releases page and add it to the classpath of your application.

Warning
If you’re using an application server like Tomcat and you’re defining a datasource on application server level, sniffy-3.1.12.jar should be added to the common classloader classpath and should be absent in web application classpath.

Non-Servlet applications

Standalone sniffy uber-jar also works with non-Servlet applications (Currently only fault tolerance testing and TCP delay are supported in this mode). Just add following parameter to your java application command line: -javaagent:sniffy-<VERSION>.jar=5559

Here 5559 is the port where connectivity controls are available - just point your browser to 5559 port on your host (Say http://localhost:5559 ). Below is an example of connections discovered in IntelliJ Idea:

agent ui

Advanced syntax takes comma separated parameters: -javaagent:sniffy-<VERSION>.jar=sniffyPort=5559,monitorNio=true

Spring Boot Integration

If you’re using Spring Boot, add the dependency below to your project in order to use Sniffy.

Maven
<dependency>
    <groupId>io.sniffy</groupId>
    <artifactId>sniffy-web</artifactId>
    <version>3.1.12</version>
</dependency>
Gradle
dependencies {
    compile 'io.sniffy:sniffy-web:3.1.12'
}

Sniffy Test

Sniffy artifacts for unit test frameworks are distributed via Maven Central repository and can be downloaded using your favorite package manager.

JUnit

Maven
<dependency>
    <groupId>io.sniffy</groupId>
    <artifactId>sniffy-junit</artifactId>
    <version>3.1.12</version>
    <scope>test</scope>
</dependency>
Gradle
dependencies {
    testCompile 'io.sniffy:sniffy-junit:3.1.12'
}

Spring Test

Maven
<dependency>
    <groupId>io.sniffy</groupId>
    <artifactId>sniffy-spring-test</artifactId>
    <version>3.1.12</version>
    <scope>test</scope>
</dependency>
Gradle
dependencies {
    testCompile 'io.sniffy:sniffy-spring-test:3.1.12'
}

TestNG

Maven
<dependency>
    <groupId>io.sniffy</groupId>
    <artifactId>sniffy-testng</artifactId>
    <version>3.1.12</version>
    <scope>test</scope>
</dependency>
Gradle
dependencies {
    testCompile 'io.sniffy:sniffy-testng:3.1.12'
}

Setup

Using Sniffy with Spring

If you are using Spring Boot, simply add @EnableSniffy to your application class:

package com.acme;

import io.sniffy.boot.EnableSniffy;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@EnableAutoConfiguration
@EnableSniffy // (1)
public class Application {

    public static void main(String[] args) throws ClassNotFoundException {
        SpringApplication.run(Application.class, args);
    }

}
  1. Put this annotation on a class with spring configuration.

It will wrap all existing datasources with SniffyDataSource and also create an instance of SniffyFilter with bean id sniffyFilter which will inject sniffy widget into HTML pages. If you are using Spring Boot embedded servlet container t is sufficient - otherwise you should also create a mapping for this filter.

Datasource

Add sniffy to classpath

In order to intercept the SQL queries executed by your application you should use Sniffy datasource wrapper. At first you should add sniffy.jar to classpath of classloader which loads actual driver. If your datasource is created by application server and registered in JNDI for later usage, you should copy sniffy.jar so it would be available by application server common classloader. For example in case of Apache Tomcat you should place it to <TOMCAT-HOME>/lib folder

Enable sniffy for datasource

In order to enable sniffy on a datasource, just add sniffy: prefix and use io.sniffy.sql.SniffyDriver as a driver class name. For example jdbc:h2:~/test should be changed to sniffy:jdbc:h2:mem: The Sniffy JDBC driver class name to io.sniffy.sql.SniffyDriver

Filter

Enable Sniffy filter in web.xml

<filter>
    <filter-name>sniffer</filter-name>
    <filter-class>io.sniffy.servlet.SniffyFilter</filter-class>
    <async-supported>true</async-supported>
    <init-param>
        <param-name>enabled</param-name> (1)
        <param-value>true</param-value> <!-- default: true -->
    </init-param>
    <init-param>
        <param-name>exclude-pattern</param-name> (2)
        <param-value>^/vets.html$</param-value> <!-- optional -->
    </init-param>
    <init-param>
        <param-name>inject-html</param-name> (3)
        <param-value>true</param-value> <!-- default: true -->
    </init-param>
    <init-param>
        <param-name>inject-html-exclude-pattern</param-name> (4)
        <param-value>^/peds.html$</param-value> <!-- optional -->
    </init-param>
    <init-param>
        <param-name>monitor-socket</param-name> (5)
        <param-value>false</param-value> <!-- default: true -->
    </init-param>
</filter>
<filter-mapping>
    <filter-name>sniffer</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
  1. Allows disabling the Sniffy filter in web.xml

  2. Allows excluding some of the request URL’s from Sniffer filter

  3. Enables injection of Sniffy toolbar to HTML. If disabled the html remains untouched. You still can get the number of executed queries from Sniffy-Sql-Queries HTTP header.

  4. Allows excluding of Sniffy toolbar injection to some of the request URL’s

  5. Allows disabling socket monitoring functionality; will also disable fault tolerance testing features

Apart from obvious false and true values, you can set inject-html and 'enabled' parameters to a system value. It will act as described in table below:

Table 1. Sniffy Filter Configuration
-Dio.sniffy.filterEnabled or IO_SNIFFY_FILTER_ENABLED enabled filter init param Effective value

null

absent

true

true

absent

true

false

absent

false

null

false

false

true

false

false

false

false

false

null

true

true

true

true

true

false

true

true

null

system

false

true

system

true

false

system

false

Similar rules are applied to inject-html filter parameter

J2EE Containers

Integration of Sniffy with Servlet / J2EE containers is pretty straightforward - you should follow the instructions for setting up Sniffy DataSource and Filter above. However some containers are using complicated ClassLoader’s and require some additional steps.

Use Sniffy with WildFly

If you want to use Sniffy with WildFly you need to add it as a module.

In order to do that unzip the sniffy-{VERSION}-jboss-module.zip (available in maven Nexus central repository as io.sniffy:sniffy:jar:jboss-module artifact) archive to modules/system/layers/base folder. Edit the modules/system/layers/base/io/sniffy/main/module.xml file and specify a module containing your actual JDBC driver:

<?xml version="1.0" encoding="UTF-8"?>
<module xmlns="urn:jboss:module:1.3" name="io.sniffy">

    <resources>
        <resource-root path="sniffy.jar"/>
    </resources>

    <dependencies>
        <!-- Try import some well known modules with JDBC drivers -->
        <module name="com.h2database.h2" optional="true"/>

        <module name="com.oracle" optional="true"/>
        <module name="oracle.jdbc" optional="true"/>
        <module name="com.oracle.jdbc" optional="true"/>
        <!-- insert a reference to module with your JDBC driver here -->

        <module name="javax.servlet.api" export="true"/>
        <module name="sun.jdk"/>
    </dependencies>
</module>

Now we can use io.sniffy module. At first we should change the DataSource configuration in standalone/configuration/standalone.xml:

<datasources>
        <datasource jndi-name="java:jboss/datasources/ExampleDS" pool-name="ExampleDS" enabled="true" use-java-context="true">
            <connection-url>sniffy:jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE</connection-url>
            <driver>sniffy</driver>
            <security>
                <user-name>sa</user-name>
                <password>sa</password>
            </security>
        </datasource>
        <drivers>
            <driver name="sniffy" module="io.sniffy">
                <xa-datasource-class>io.sniffy.sql.SniffyDataSource</xa-datasource-class>
            </driver>
        </drivers>
</datasources>

Finally we should import io.sniffy module to your web application in order to be able to use SniffyFilter. Edit the jboss-deployment-structure.xml file inside your web application and add a reference to io.sniffy module:

<jboss-deployment-structure>
    <deployment>
        <dependencies>
            <module name="io.sniffy"/>
        </dependencies>
    </deployment>
</jboss-deployment-structure>

Use Sniffy with Tomcat

If you’re using datasources managed within Tomcat rather that within your web application, place sniffy-<VERSION>.jar to the lib folder And add sniffy: prefix to the Datasource configuration in conf/server.xml file. For example

<Resource name="jdbc/myoracle" auth="Container"
    type="javax.sql.DataSource" driverClassName="io.sniffy.sql.SniffyDriver"
    url="sniffy:jdbc:oracle:thin:@127.0.0.1:1521:mysid"
    username="scott" password="tiger" maxTotal="20" maxIdle="10"
    maxWaitMillis="-1"/>
Warning
Make sure that in this setup you do not have sniffy jars in your web application class path (i.e. WEB-INF/lib folder)

Configuration

Sniffy can be configured globally using Java system properties or environment variables. This configuration can be overriden in web.xml and/or @EnableSniffy annotation - see appropriate sections of documentation.

Warning
sniffy configuration is parsed only once and any changes made to system properties or environment variables in run-time won’t have any effect on Sniffy
Table 2. Table Configuration properties
System Property Environment Variable Sniffy Agent parameter Description Default Value

-Dio.sniffy.logLevel

IO_SNIFFY_LOG_LEVEL

N/A

Sniffy logging level; allowed values are: TRACE, DEBUG, INFO, ERROR, OFF

info

-Dio.sniffy.monitorJdbc

IO_SNIFFY_MONITOR_JDBC

N/A

Monitor JDBC

true

-Dio.sniffy.monitorSocket

IO_SNIFFY_MONITOR_SOCKET

N/A

Monitor socket connections

false (enabled implicitly by Sniffy javaagent or SniffyFiler)

-Dio.sniffy.monitorNio

IO_SNIFFY_MONITOR_NIO

monitorNio

Monitor NIO socket connections

false (enabled implicitly by Sniffy javaagent or SniffyFiler)

-Dio.sniffy.topSqlCapacity

IO_SNIFFY_TOP_SQL_CAPACITY

N/A

Maximum number of top SQL queries to store

1024

-Dio.sniffy.packetMergeThreshold

IO_SNIFFY_PACKET_MERGE_THRESHOLD

N/A

Threshold for combining similar network packets when capturing traffic

500

-Dio.sniffy.filterEnabled

IO_SNIFFY_FILTER_ENABLED

N/A

Enable servlet filter

true

-Dio.sniffy.excludePattern

IO_SNIFFY_EXCLUDE_PATTERN

N/A

Regexp for excluding sniffy completely from certain servlet requests

-Dio.sniffy.injectHtml

IO_SNIFFY_INJECT_HTML

N/A

Inject Sniffy HTML to result HTML

true

-Dio.sniffy.injectHtmlExcludePattern

IO_SNIFFY_INJECT_HTML_EXCLUDE_PATTERN

N/A

Regexp for excluding sniffy widget from certain servlet requests

N/A

N/A

sniffyPort

HTTP port Sniffy Agent is listening on

5555

-Dio.sniffy.socketCaptureEnabled

IO_SNIFFY_SOCKET_CAPTURE_ENABLED

N/A

Enable collecting network stats (bytes sent/received, time spent) if monitorSocket or monitorNio are enabled

true

-Dio.sniffy.decryptTls

IO_SNIFFY_DECRYPT_TLS

N/A

Enable decrypting captured TLS traffic

false

-Dio.sniffy.socketFaultInjectionEnabled

IO_SNIFFY_SOCKET_FAULT_INJECTION_ENABLED

N/A

Enable injecting network faults if monitorSocket or monitorNio are enabled

true

Sniffy filter can also be enabled or disabled using HTTP query parameters and/or HTTP headers.

If Sniffy filter is currently disabled you can enable it by adding ?sniffy=true query parameter to your request - it will enable the sniffy for current request and will also set a sniffy cookie which will enable sniffy on subsequent requests.

For stateless clients who don’t maintain the cookie jar it might be more convenient to enable/disable Sniffy using Sniffy-Enabled: true / Sniffy-Enabled: false headers. Unlike the query parameter the HTTP header will only affect a single request.

A similar header Sniffy-Inject-Html-Enabled can be used for hiding or showing the Sniffy Widget in the browser. Unlike Sniffy-Enabled It doesn’t come with a query parameter alternative.

Convenient Sniffy Chrome Extension can add these headers by simply clicking on an icon in your Google Chrome browser.

Unit and component tests

Using Sniffy API

Sniffy provides a convenient API for validating the number of executed database queries, affected database rows or even number of active TCP connections. The main classes you should use are io.sniffy.Sniffy and io.sniffy.Spy.

Spy objects are responsible for recording the executed queries and bytes sent over the wire. Spy stores all the information since the moment it was created. Sniffy class provides convenient factory methods for creating Spy instances

Imperative approach

        Connection connection = DriverManager.getConnection("sniffy:jdbc:h2:mem:", "sa", "sa"); // (1)
        Spy<?> spy = Sniffy.spy(); // (2)
        connection.createStatement().execute("SELECT 1 FROM DUAL"); // (3)
        spy.verify(SqlQueries.atMostOneQuery()); // (4)
        spy.verify(SqlQueries.noneQueries().otherThreads()); // (5)
  1. Just add sniffy: in front of your JDBC connection URL in order to enable sniffer.

  2. Spy holds the amount of queries executed till the given amount of time. It acts as a base for further assertions.

  3. You do not need to modify your JDBC code.

  4. spy.verify(SqlQueries.atMostOneQuery()) throws an AssertionError if more than one query was executed.

  5. spy.verify(SqlQueries.noneQueries().otherThreads()) throws an AssertionError if at least one query was executed by the thread other than then current one.

Functional approach

        final Connection connection = DriverManager.getConnection("sniffy:jdbc:h2:mem:", "sa", "sa"); // (1)
        Sniffy.execute(
                () -> connection.createStatement().execute("SELECT 1 FROM DUAL")
        ).verify(SqlQueries.atMostOneQuery()); // (2)
  1. Just add sniffy: in front of your JDBC connection URL in order to enable sniffer.

  2. Sniffy.execute() method executes the lambda expression and returns an instance of Spy which provides methods for validating the number of executed queries in given lambda/

Resource approach

        final Connection connection = DriverManager.getConnection("sniffy:jdbc:h2:mem:", "sa", "sa"); // (1)
        try (@SuppressWarnings("unused") Spy s = Sniffy. // (2)
                expect(SqlQueries.atMostOneQuery()).
                expect(SqlQueries.noneQueries().otherThreads());
             Statement statement = connection.createStatement()) {
            statement.execute("SELECT 1 FROM DUAL");
        }
  1. Just add sniffy: in front of your JDBC connection URL in order to enable sniffer.

  2. You can use Sniffy in a try-with-resource block using expect methods instead of verify. When the try-with-resource block is completed, Sniffy will verify all the expectations defined

Shared connection

In some test scenarios you might want to execute all SQL commands in a single connection even though your code under test obtains multiple connections. The major use case is starting a transaction in the beginning of your test and rolling it back at the end. Using shared connection will force you application to use this particular transaction and connection even if you work will multiple threads/transactions/connections.

SharedConnectionDataSource is a wrapper around any other DataSource with two additional methods:

public SharedConnectionDataSource(DataSource targetDataSource);

public synchronized void setCurrentThreadAsMaster();

public synchronized void resetMasterConnection() throws InterruptedException;

Call setCurrentThreadAsMaster() method when you want to switch your DataSource to a shared connection mode. After you call this method, all new connections obtained from this DataSourcce will actually reuse the same underlying connection

Call resetMasterConnection() when your test is finished and you want to return the data source to the ordinary state.

Master and slave connections

In SharedConnectionDataSource there’s one master connection and multiple slave connections. All operations affecting the connection (such as close, commit, rollback, e.t.c.) are suppressed in slave connections, i.e. they do nothing.

This how master connection is determined: - If there is an active connection obtained in the same thread as one calling setCurrentThreadAsMaster(), this connection is marked as master - Otherwise the first connection obtained in the same thread as one calling setCurrentThreadAsMaster() will be marked as master

Example

        SharedConnectionDataSource sharedConnectionDataSource = new SharedConnectionDataSource(targetDataSource); // (1)

        sharedConnectionDataSource.setCurrentThreadAsMaster(); // (2)

        try (Connection masterConnection = sharedConnectionDataSource.getConnection(); // (3)
             Connection slaveConnection = newSingleThreadExecutor().submit(
                     (Callable<Connection>) sharedConnectionDataSource::getConnection).get() // (4)
        ) {
            assertEquals(masterConnection, slaveConnection); // (5)
        } finally {
            sharedConnectionDataSource.resetMasterConnection(); // (6)
        }
  1. Create a SharedConnectionDataSource wrapper by passing an underlying DataSource to the constructor

  2. Mark current Thread as master - first connection obtained from this thread will be considered master

  3. Get the master connection from the pool

  4. Get the slave connection from the pool. It is slave cause called from another thread and master connection is already determined

  5. Assert that connections are actually the same

  6. Release the master connection. This call will for all slave connetions to finish (until close() method is called) and unmark current thread and connection as master

Caveats

Although all API calls which might affect the shared connection in slave connections, it is still possible to interfere with other connections. For example any DDL calls (like CREATE TABLE) commit the transaction implicitly.

Integration with JUnit

Sniffy comes with a JUnit @Rule for quick integration with test framework. Just add @Rule public final SniffyRule sniffyRule = new SniffyRule(); to your JUnit test class and place appropriate expectations on your test methods like shown below.

package io.sniffy.test.junit.usage;

import io.sniffy.socket.DisableSockets;
import io.sniffy.sql.SqlExpectation;
import io.sniffy.test.Count;
import io.sniffy.test.junit.SniffyRule;
import org.junit.Rule;
import org.junit.Test;

import java.io.IOException;
import java.net.ConnectException;
import java.net.Socket;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;

public class JUnitUsageTest {

    @Rule
    public final SniffyRule sniffyRule = new SniffyRule(); // (1)

    @Test
    @SqlExpectation(count = @Count(1)) // (2)
    public void testJUnitIntegration() throws SQLException {
        final Connection connection = DriverManager.getConnection("sniffy:jdbc:h2:mem:", "sa", "sa"); // (3)
        connection.createStatement().execute("SELECT 1 FROM DUAL"); // (4)
    }

    @Test
    @DisableSockets // (5)
    public void testDisableSockets() throws IOException {
        try {
            new Socket("google.com", 22); // (6)
            fail("Sniffy should have thrown ConnectException");
        } catch (ConnectException e) {
            assertNotNull(e);
        }
    }

}
  1. - Integrate Sniffy to your test using @Rule annotation and a SniffyRule field.

  2. - Now just add @SqlExpectation annotation to define number of queries allowed for given method.

  3. - Just add sniffy: in front of your JDBC connection URL in order to enable sniffer.

  4. - Do not make any changes in your code - just add the @Rule SniffyRule and put annotations on your test method.

  5. - Add @DisableSockets annotation on your test method or test class and any attempt to open a network connection will fail

  6. - All socket operations executed within test method annotated with @DisableSockets will throw a java.net.ConnectException

Integration with Kotest

Sniffy comes with a Kotest TestCaseExtension`s for quick integration with test framework. Just add `DisableSocketsExtension to simulate network isolation in your tests or SniffyExtension to assert number of executed queries and/or opened connections

package io.sniffy.test.kotest.usage

import com.hazelcast.client.HazelcastClient
import com.hazelcast.client.config.*
import com.hazelcast.config.Config
import com.hazelcast.config.NetworkConfig
import com.hazelcast.core.Hazelcast
import com.hazelcast.core.HazelcastInstance
import io.kotest.assertions.fail
import io.kotest.core.extensions.TestCaseExtension
import io.kotest.core.spec.style.StringSpec
import io.kotest.core.test.TestCase
import io.kotest.core.test.TestResult
import io.kotest.core.test.TestStatus
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import io.kotest.matchers.types.instanceOf
import io.ktor.client.*
import io.ktor.client.engine.apache.*
import io.ktor.client.request.*
import io.sniffy.SniffyAssertionError

class ExpectSniffyAssertionExceptionExtension : TestCaseExtension {

    override suspend fun intercept(testCase: TestCase, execute: suspend (TestCase) -> TestResult): TestResult {
        val testResult = execute(testCase)
        try {
            testResult.error shouldBe instanceOf(SniffyAssertionError::class)
        } catch (e: Exception) {
            return testResult.copy(status = TestStatus.Failure, error = e)
        }
        return TestResult.success(testResult.duration)
    }
}

class KotestUsageTests : StringSpec({

    @Suppress("BlockingMethodInNonBlockingContext")
    "Ktor HTTP Client should be intercepted by Sniffy".config(extensions = listOf( // (1)
            ExpectSniffyAssertionExceptionExtension(),
            NoSocketsAllowedExtension() // (2)
    )) {

        val client = HttpClient(Apache)

        client.get("https://en.wikipedia.org/wiki/Main_Page")

    }

    "Hazelcast client should be intercepted by Sniffy".config(
            extensions = listOf(DisableSocketsExtension())) {// (3)

        val serverConfig: Config = Config("my-hazelcast").apply {
            networkConfig = NetworkConfig().apply {
                port = 6600
                portCount = 1
            }
        }

        val hzInstance: HazelcastInstance = Hazelcast.newHazelcastInstance(serverConfig)
        hzInstance.getMap<Any, Any>("my-distributed-map").put("key", "value")

        try {
            val config = ClientConfig().apply {
                instanceName = "my-hazelcast"
                networkConfig = ClientNetworkConfig().apply {
                    addresses = listOf("localhost:6600")
                }
                connectionStrategyConfig = ClientConnectionStrategyConfig().apply {
                    connectionRetryConfig = ConnectionRetryConfig().apply {
                        initialBackoffMillis = 50
                        maxBackoffMillis = 100
                        clusterConnectTimeoutMillis = 200
                    }
                }
            }
            val failoverConfig = ClientFailoverConfig().apply {
                tryCount = 1
                clientConfigs = listOf(config)
            }
            val hazelcastClient = HazelcastClient.newHazelcastFailoverClient(failoverConfig)
            hazelcastClient.getMap<Any, Any>("my-distributed-map").get("key") shouldBe "value"
            fail("Should have been refused by Sniffy")
        } catch (e: Exception) {
            e shouldNotBe null
        } finally {
            hzInstance.shutdown()
        }

    }

})
  1. - Integrate Sniffy to your test by adding appropriate extensions

  2. - NoSocketsAllowedExtension would fail your test if it accesses network

  3. - DisableSocketsExtension can be used for simulating network isolation for your code

Integration with Spring Framework

Sniffy comes with a Spring Framework via SniffySpringTestListener spring @TestExecutionListener. Just add @TestExecutionListeners(SniffySpringTestListener.class) to your Spring test class and place appropriate expectations on your test methods like shown below.

package io.sniffy.test.spring.usage;

import io.sniffy.socket.DisableSockets;
import io.sniffy.sql.SqlExpectation;
import io.sniffy.test.Count;
import io.sniffy.test.spring.SniffySpringTestListener;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import java.io.IOException;
import java.net.ConnectException;
import java.net.Socket;
import java.sql.Connection;
import java.sql.SQLException;

import static java.sql.DriverManager.getConnection;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
import static org.springframework.test.context.TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringUsageTest.class)
@TestExecutionListeners(value = SniffySpringTestListener.class, mergeMode = MERGE_WITH_DEFAULTS) // (1)
public class SpringUsageTest {

    @Test
    @SqlExpectation(count = @Count(max = 1))
    public void testJUnitIntegration() throws SQLException {
        final Connection connection = getConnection(
                "sniffy:jdbc:h2:mem:", "sa", "sa");
        connection.createStatement().execute("SELECT 1 FROM DUAL");
    }

    @Test
    @DisableSockets // (5)
    public void testDisableSockets() throws IOException {
        try {
            new Socket("google.com", 443); // (6)
            fail("Sniffy should have thrown ConnectException");
        } catch (ConnectException e) {
            assertNotNull(e);
        }
    }

}
  1. - Integrate Sniffy to your test using @TestExecutionListeners(SniffySpringTestListener.class).

  2. - Now just add @SqlExpectation annotation to define number of queries allowed for given method.

  3. - Just add sniffy: in front of your JDBC connection URL in order to enable sniffer.

  4. - Do not make any changes in your code - just add the @TestExecutionListeners(SniffySpringTestListener.class) and put annotations on your test method.

  5. - Add @DisableSockets annotation on your test method or test class and any attempt to open a network connection will fail

  6. - All socket operations executed within test method annotated with @DisableSockets will throw a java.net.ConnectException

@SharedConnection

Sniffy provides convenient annotations for shared connection data source in Spring unit tests. Consider example below:

package io.sniffy.test.spring.usage;

import io.sniffy.test.spring.DataSourceTestConfiguration;
import io.sniffy.test.spring.EnableSharedConnection;
import io.sniffy.test.spring.SharedConnection;
import io.sniffy.test.spring.SniffySpringTestListener;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.annotation.Transactional;
import ru.yandex.qatools.allure.annotations.Features;

import java.sql.SQLException;
import java.util.Arrays;
import java.util.concurrent.ExecutionException;

import static java.util.concurrent.Executors.newSingleThreadExecutor;
import static org.junit.Assert.assertEquals;
import static org.springframework.test.context.TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {DataSourceTestConfiguration.class, SpringSharedConnectionUsageTest.class})
@TestExecutionListeners(value = SniffySpringTestListener.class, mergeMode = MERGE_WITH_DEFAULTS) // (1)
@EnableSharedConnection // (2)
@Transactional
@Rollback
public class SpringSharedConnectionUsageTest {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Test
    @SharedConnection // (3)
    @Features("issue/344")
    public void testSharedConnectionTwoRows() throws SQLException, ExecutionException, InterruptedException {

        jdbcTemplate.batchUpdate(
                "INSERT INTO PUBLIC.PROJECT (ID, NAME) VALUES (SEQ_PROJECT.NEXTVAL, ?)",
                Arrays.asList(new Object[]{"foo"}, new Object[]{"bar"})
        ); // (4)

        assertEquals(2, newSingleThreadExecutor().submit(
                () -> jdbcTemplate.queryForObject("SELECT COUNT(*) FROM PUBLIC.PROJECT", Integer.class) // (5)
        ).get().intValue());

    }

}
  1. - Integrate Sniffy to your test using @TestExecutionListeners(SniffySpringTestListener.class).

  2. - @EnableSharedConnection will automatically wrap all your data sources with SharedConnectionDataSource

  3. - @SharedConnection annotation will make the current connection (started because of @Transactional annotation before each test) as master

  4. - Insert two rows into table using master connection

  5. - Another slave connection obtained in another thread will still see the results of these inserts although the isolation level is READ_COMITTED and master transaction has not been committed

Troubleshooting

  1. Spring test context is no longer loaded when I add @TestExecutionListeners(SniffySpringTestListener.class)

The reason is that by default Spring does not merge the test execution listeners. Using this configuration you’re removing the predefined listeners which initialize the context. Change the mergeMode parameter to MERGE_WITH_DEFAULTS as shown below:

@TestExecutionListeners(value = SniffySpringTestListener.class, mergeMode = MERGE_WITH_DEFAULTS)

Integration with Test NG

Sniffy comes with a Test NG listener for quick integration with test framework. Just add @Listeners(SniffyTestNgListener.class) to your TestNG test class and place appropriate expectations on your test methods like shown below.

package io.sniffy.test.testng.usage;

import io.sniffy.socket.DisableSockets;
import io.sniffy.sql.SqlExpectation;
import io.sniffy.test.Count;
import io.sniffy.test.testng.SniffyTestNgListener;
import org.testng.annotations.Listeners;
import org.testng.annotations.Test;

import java.io.IOException;
import java.net.ConnectException;
import java.net.Socket;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;

@Listeners(SniffyTestNgListener.class) // (1)
public class UsageTestNg {

    @Test
    @SqlExpectation(count = @Count(1)) // (2)
    public void testJUnitIntegration() throws SQLException {
        final Connection connection = DriverManager.getConnection("sniffy:jdbc:h2:mem:", "sa", "sa"); // (3)
        connection.createStatement().execute("SELECT 1 FROM DUAL"); // (4)
    }

    @Test
    @DisableSockets // (5)
    public void testDisableSockets() throws IOException {
        try {
            new Socket("google.com", 22); // (6)
            fail("Sniffy should have thrown ConnectException");
        } catch (ConnectException e) {
            assertNotNull(e);
        }
    }

}
  1. - Integrate Sniffy to your test using @Listeners(SniffyTestNgListener.class).

  2. - Now just add @SqlExpectation annotation to define number of queries allowed for given method.

  3. - Just add sniffy: in front of your JDBC connection URL in order to enable sniffer.

  4. - Do not make any changes in your code - just add the @Listeners(SniffyTestNgListener.class) and put annotations on your test method.

  5. - Add @DisableSockets annotation on your test method or test class and any attempt to open a network connection will fail

  6. - All socket operations executed within test method annotated with @DisableSockets will throw a java.net.ConnectException

Integration with Spock Framework

Spock Framework is a developer testing and specification framework for Java and Groovy applications.

Sniffy can be integrated with Spock Framework using Spy field and standard spock then block:

package io.sniffy.test.spock.usage

import groovy.sql.Sql
import io.sniffy.Sniffy
import io.sniffy.sql.SqlQueries
import io.sniffy.sql.WrongNumberOfQueriesError
import spock.lang.FailsWith
import spock.lang.Shared
import spock.lang.Specification

class SpockUsageSpec extends Specification {

    @Shared sql = Sql.newInstance("sniffy:jdbc:h2:mem:", "sa", "sa")

    def spy = Sniffy.spy()

    @FailsWith(WrongNumberOfQueriesError)
    "Execute single query - negative"() {
        when:
        sql.execute("SELECT 1 FROM DUAL")
        sql.execute("SELECT 1 FROM DUAL")

        then:
        spy.verify(SqlQueries.exactQueries(1))
    }

    def "Execute single query"() {
        when:
        sql.execute("SELECT 1 FROM DUAL")

        then:
        spy.verify(SqlQueries.exactQueries(1)).reset()

        when:
        sql.execute("SELECT 1 FROM DUAL")

        then:
        spy.verify(SqlQueries.exactQueries(1))
    }

    def "Execute single query - another one"() {
        when:
        sql.execute("SELECT 1 FROM DUAL")

        then:
        spy.verify(SqlQueries.exactQueries(1))
    }

}

Do not forget to call reset() method on the spy object if you have multiple when-then blocks in a single test method

Emulating network issues

Sniffy uses heuristics to add delay for TCP connections.

Say we have specified a delay of D milliseconds and have the R receive buffer and S send buffer

Capturing traffic

Capture Traffic

Sniffy allows you to record traffic sent or received via network and exposes it via Java API.

Example

    @Test
    public void testCaptureTraffic() throws Exception {

        try (Spy<?> spy = Sniffy.spy(
                SpyConfiguration.builder().captureNetworkTraffic(true).build()) // (1)
        ) {

            performSocketOperation(); // (2)

            Map<SocketMetaData, List<NetworkPacket>> networkTraffic = spy.getNetworkTraffic( // (3)
                    Threads.ANY, // (4)
                    AddressMatchers.anyAddressMatcher(), // (5)
                    GroupingOptions.builder().
                            groupByThread(false). // (6)
                            groupByStackTrace(false). // (7)
                            groupByConnection(false). // (8)
                            build()
            );

            assertEquals(1, networkTraffic.size());

            for (Map.Entry<SocketMetaData, List<NetworkPacket>> entry : networkTraffic.entrySet()) {

                SocketMetaData socketMetaData = entry.getKey(); // (9)

                Protocol protocol = socketMetaData.getProtocol(); // say TCP
                String hostName = socketMetaData.getAddress().getHostName(); // say "hostname.acme.com"
                int port = socketMetaData.getAddress().getPort(); // say 443

                List<NetworkPacket> networkPackets = entry.getValue(); // (10)

                assertArrayEquals(REQUEST, networkPackets.get(0).getBytes());
                assertTrue(networkPackets.get(0).isSent());

                assertArrayEquals(RESPONSE, networkPackets.get(1).getBytes());
                assertFalse(networkPackets.get(1).isSent());

            }

        }

    }
  1. Use captureNetworkTraffic(true) when creating a Spy instance to enable traffic capturing

  2. Invoke socket operation. Can be either in current or another thread. Plain socket API or NIO - doesn’t matter.

  3. Retrieve network packets filtered and grouped as defined in parameters

  4. Allows you to filter traffic by thread

  5. Allows you to filter traffic by target address and/or port

  6. Group traffic by thread

  7. Group traffic by stack trace

  8. Group traffic by connection id - surrogate identifier of network connection

  9. SocketMetaData describes the target host and port and other parameters as described above for GroupingOptions

  10. NetworkPacket contains data in bytes, timestamp, direction and optional stacktrace and thread information

Configuration

packetMergeThreshold allows you to specify threshold for combining similar network packets when capturing traffic. For example if your application calls SocketInputStream.read() 8 times in a row to read a 64-bit value, Sniffy will merge these calls into a single packet given that they were done within specified threshold and of course given that no write/sent operation was done between them.

SSL/TLS Traffic Decryption

Sniffy can decrypt capture traffic but you need to enable this feature explicitly using -D`io.sniffy.decryptTls` system property or IO_SNIFFY_DECRYPT_TLS environment variable. It’s also possible to enable it programmatically using io.sniffy.configuration.SniffyConfiguration.INSTANCE.setDecryptTls() method.

Decrypted traffic is available along with original traffic using dedicated getDecryptedNetworkTraffic method in Spy class.

Map<SocketMetaData, List<NetworkPacket>> decryptedNetworkTraffic = spy.getDecryptedNetworkTraffic(
        Threads.CURRENT,
        AddressMatchers.exactAddressMatcher("www.google.com:443"),
        GroupingOptions.builder().
                groupByConnection(false).
                groupByStackTrace(false).
                groupByThread(false).
                build()
);

Caveats

Some libraries like Apache APR and Netty (only when native transport is enabled explicitly) use native code for network operations. Currently Sniffy doesn’t capture this kind of traffic.

Sniffy provides an alternative implementation of SSLEngine, SSLSocketFactory and other classes from JSSE framework. Since some of these constructs can be cached in application code, please make sure to initialize Sniffy as early as possible. If absolutely necessary you can initialize Sniffy as a javaagent, although it’s a bit cumbersome - see Standalone Setup section for details.

Sniffy support custom JSSE providers such as BouncyCastle. However if you’re installing AFTER you initialize Sniffy you might need to reinitialize Sniffy. It’s possible using Sniffy.reinitialize() method.e

Migration from previous versions

Migration from 3.0.x to 3.1.x

Sniffy Filter

Starting from version 3.1.0 injection of HTML is no longer considered experimental and enabled by default.

Maven artifacts

Sniffy test support has been extracted to a separate artifacts. You should now use following artifacts if you want to use Sniffy in your unit tests:

Table 3. Table Maven artifacts migration
Old artifact New artifact Test framework

io.sniffy:sniffy:test

io.sniffy:sniffy-junit:test

JUnit

io.sniffy:sniffy:test

io.sniffy:sniffy-spring-test:test

Spring Framework

io.sniffy:sniffy:test

io.sniffy:sniffy-testng:test

TestNG

io.sniffy:sniffy:test

io.sniffy:sniffy-core:test

Spock Framework

JDBC Connection String

sniffer: connection is deprecated as of Sniffy 3.1.0. You should use sniffy: instead like shown below:

sniffy:jdbc:h2:mem:

Deprecated Classes

Some of Sniffy classes are deprecated as of version 3.1.0 with an equivalent replacement as shown in the table below:

Table 4. Table Sniffy 3.1.0 deprecated classes
Deprecated class New class

io.sniffy.MockDriver

io.sniffy.sql.SniffyDriver

io.sniffy.Query

io.sniffy.sql.SqlStatement

io.sniffy.Sniffer

io.sniffy.Sniffy

io.sniffy.WrongNumberOfQueriesError

io.sniffy.sql.WrongNumberOfQueriesError

io.sniffy.servlet.SnifferFilter

io.sniffy.servlet.SniffyFilter

io.sniffy.junit.QueryCounter

io.sniffy.test.junit.SniffyRule

io.sniffy.spring.QueryCounterListener

io.sniffy.test.spring.SniffySpringTestListener

io.sniffy.Expectation

io.sniffy.sql.SqlExpectation

io.sniffy.Expectations

io.sniffy.sql.SqlExpectations

io.sniffy.NoQueriesAllowed

io.sniffy.sql.NoSql

io.sniffy.testng.QueryCounter

io.sniffy.test.testng.SniffyTestNgListener

Deprecated Methods

io.sniffy.Sniffer.*

Some methods in io.sniffy.Sniffer class are now deprecated and although they’re still available in io.sniffy.Sniffy class they will be removed completely in future versions of Sniffy.

Table 5. Table io.sniffy.Sniffer deprecated methods
Deprecated method Replacement

executedStatements()

spy().getExecutedStatements(Threads threadMatcher, boolean removeStackTraces)

expect*(…​)

expect(Spy.Expectation expectation)

io.sniffy.Spy.*

Some methods in io.sniffy.Spy class are now deprecated and will be removed completely in future versions of Sniffy.

Table 6. Table io.sniffy.Spy deprecated methods
Deprecated method Replacement

executedStatements()

spy().getExecutedStatements(Threads threadMatcher, boolean removeStackTraces)

expect*(…​)

expect(Spy.Expectation expectation)

verify*(…​)

verify(Spy.Expectation expectation)

Deprecated annotations

@EnableSniffy(excludePattern="…​") has been deprecated in favor of @EnableSniffy(advanced = @SniffyAdvancedConfiguration(excludePattern = "…​",))