Test-Driven Development with SNMP4J
This article presents a simple example of test-driven development with SNMP4J. Unit testing goes hand in hand with TDD. Agile testing is designed to prevent bugs and relies primarily on unit tests. Comprehensive regression testing can be run several times in a day. A test suite prevents fixed bugs from coming back or finding side effects of changes. The source of bugs can be pinpointed.
The rhythm of coding in TDD is test a little… code a little… test a little… code a little… test a little… code a little…
We will follow the 5 steps of TDD in creating our project.
- Add a Test
- Watch the Test Fail
- Write the Code
- Run the Tests
- Refactor
1. Prerequisites
- SNMP4J and SNMP4J Agent libraries
- Mars Eclipse
- Have read SNMPv3 Set Example using SNMP4J
- Familiarity with Apache Maven.
2. Create the Project
Before anything else, we must create a Maven project in Eclipse. Click on File -> New -> Maven Project to create the project. Check the Create a simple project -> Next. Enter com.javacodegeeks.example
for our groupId
and snmp4j-unit-test
for our artifactId
. Lastly, add the following dependencies:
- snmp4j
- snmp4j-agent with a test scope
- junit with a test scope
Our project object model should look like this:
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.javacodegeeks.example</groupId> <artifactId>snmp4j-unit-test</artifactId> <version>0.0.1-SNAPSHOT</version> <dependencies> <dependency> <groupId>org.snmp4j</groupId> <artifactId>snmp4j</artifactId> <version>2.5.6</version> </dependency> <dependency> <groupId>org.snmp4j</groupId> <artifactId>snmp4j-agent</artifactId> <version>2.5.3</version> <scope>test</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> </dependencies> </project>
3. Add a Test
Create a package called com.javacodegeeks.example
under the src/test/java folder. In this package we will create our test class named SnmpClientTest.java
. We’ll just keep our requirements simple. Our client will just do a simple SNMP Get Request. Our final test class will look like the one below:
SnmpClientTest.java
package com.javacodegeeks.example; import static org.junit.Assert.assertEquals; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; public class SnmpClientTest { static MyAgent agent; @BeforeClass public static void setup() throws Exception { agent = new MyAgent(); agent.start(); } @AfterClass public static void tearDown() { agent.stop(); } @Test public void testGet() throws Exception { String actual = SnmpClient.get("127.0.0.1", 161, ".1.3.6.1.2.1.1.1.0"); assertEquals("My Agent System Description", actual); } }
Don’t worry about MyAgent
just yet, we will be adding that later. In the simplest sense, our test class should only have the testGet
method (highlighted). Go ahead and create a bare bones SnmpClient.java
under the src/main/java folder with the same package name. The SnmpClient
should have a static method named get
and accepts three parameters then returns a string. For now, it should not contain any implementation. Let’s just make it return an empty string. This is our class under test. Our target now is to satisfy this test. The “actual” result returned from the SNMP Get Request must equal the one we are expecting which is “My Agent System Description”. On to the next step.
4. Watch the Test Fail
Let’s check and see if our test is working properly. This test should fail because we have no implementation for it yet. This proves that the test does not pass without requiring new code.
Our “mvn test” output should look something like this:
mvn test
------------------------------------------------------- T E S T S ------------------------------------------------------- Running com.javacodegeeks.example.SnmpClientTest Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 1.43 sec <<< FAILURE! testGet(com.javacodegeeks.example.SnmpClientTest) Time elapsed: 0.161 sec <<< FAILURE!
Before implementing our code to satisfy the test, we need to create a test SNMP agent that will provide mock data to our SNMP client. Below is our test agent:
MyAgent.java
package com.javacodegeeks.example; import java.io.File; import java.io.IOException; import org.snmp4j.TransportMapping; import org.snmp4j.agent.BaseAgent; import org.snmp4j.agent.CommandProcessor; import org.snmp4j.agent.DuplicateRegistrationException; import org.snmp4j.agent.mo.MOAccessImpl; import org.snmp4j.agent.mo.MOScalar; import org.snmp4j.agent.mo.snmp.RowStatus; import org.snmp4j.agent.mo.snmp.SnmpCommunityMIB; import org.snmp4j.agent.mo.snmp.SnmpNotificationMIB; import org.snmp4j.agent.mo.snmp.SnmpTargetMIB; import org.snmp4j.agent.mo.snmp.StorageType; import org.snmp4j.agent.mo.snmp.VacmMIB; import org.snmp4j.agent.security.MutableVACM; import org.snmp4j.mp.MPv3; import org.snmp4j.security.SecurityLevel; import org.snmp4j.security.SecurityModel; import org.snmp4j.security.USM; import org.snmp4j.smi.Address; import org.snmp4j.smi.GenericAddress; import org.snmp4j.smi.Integer32; import org.snmp4j.smi.OID; import org.snmp4j.smi.OctetString; import org.snmp4j.smi.Variable; import org.snmp4j.transport.TransportMappings; public class MyAgent extends BaseAgent { public MyAgent() { super(new File("bootCounterFile.txt"), new File("configFile.txt"), new CommandProcessor(new OctetString(MPv3.createLocalEngineID()))); } @Override protected void initTransportMappings() throws IOException { transportMappings = new TransportMapping<?>[1]; Address addr = GenericAddress.parse("0.0.0.0/161"); TransportMapping<? extends Address> tm = TransportMappings.getInstance().createTransportMapping(addr); transportMappings[0] = tm; } public void start() throws IOException { init(); addShutdownHook(); getServer().addContext(new OctetString("public")); finishInit(); run(); sendColdStartNotification(); } @Override protected void registerManagedObjects() { getSnmpv2MIB().unregisterMOs(server, getContext(getSnmpv2MIB())); MOScalar mo = new MOScalar(new OID(".1.3.6.1.2.1.1.1.0"), MOAccessImpl.ACCESS_READ_ONLY, new OctetString("My Agent System Description")); try { server.register(mo, null); } catch (DuplicateRegistrationException e) { e.printStackTrace(); } } @Override protected void unregisterManagedObjects() { // do nothing } @Override protected void addUsmUser(USM usm) { // do nothing } @Override protected void addNotificationTargets(SnmpTargetMIB targetMIB, SnmpNotificationMIB notificationMIB) { // do nothing } @Override protected void addViews(VacmMIB vacmMIB) { vacmMIB.addGroup(SecurityModel.SECURITY_MODEL_SNMPv2c, new OctetString("cpublic"), new OctetString("v1v2group"), StorageType.nonVolatile); vacmMIB.addAccess(new OctetString("v1v2group"), new OctetString("public"), SecurityModel.SECURITY_MODEL_ANY, SecurityLevel.NOAUTH_NOPRIV, MutableVACM.VACM_MATCH_EXACT, new OctetString("fullReadView"), new OctetString("fullWriteView"), new OctetString("fullNotifyView"), StorageType.nonVolatile); vacmMIB.addViewTreeFamily(new OctetString("fullReadView"), new OID(".1.3"), new OctetString(), VacmMIB.vacmViewIncluded, StorageType.nonVolatile); } @Override protected void addCommunities(SnmpCommunityMIB communityMIB) { Variable[] com2sec = new Variable[] { new OctetString("public"), // community name new OctetString("cpublic"), // security name getAgent().getContextEngineID(), // local engine ID new OctetString("public"), // default context name new OctetString(), // transport tag new Integer32(StorageType.nonVolatile), // storage type new Integer32(RowStatus.active) // row status }; SnmpCommunityMIB.SnmpCommunityEntryRow row = communityMIB.getSnmpCommunityEntry().createRow( new OctetString("public2public").toSubIndex(true), com2sec); communityMIB.getSnmpCommunityEntry().addRow(row); } }
Place MyAgent
under the src/test/java com.javacodegeeks.example
package. Our agent extends the BaseAgent
of SNMP4J. The files in the constructor do not exist yet but will be created by the test agent. The things to note are highlighted. We are using 0.0.0.0
instead of the localhost IP address. Our read community is public. In the registerManagedObjects
method, we removed the SNMPv2-MIB (installed by default) and replaced it with our own scalar managed object. The value of this object is what our client will be requesting.
The addCommunities
method is the minimal implementation for the table of community strings configured in the SNMP Local Configuration Datastore. The addViews
method provides the minimal view based access control. We’re not implementing addUsmUser
because will be using SNMPv2c and not SNMPv3.
5. Write the Code
Finally, we will write the code that will satisfy the test.
SnmpClient.java
package com.javacodegeeks.example; import org.snmp4j.CommunityTarget; import org.snmp4j.PDU; import org.snmp4j.Snmp; import org.snmp4j.TransportMapping; import org.snmp4j.event.ResponseEvent; import org.snmp4j.mp.SnmpConstants; import org.snmp4j.smi.Address; import org.snmp4j.smi.GenericAddress; import org.snmp4j.smi.OID; import org.snmp4j.smi.OctetString; import org.snmp4j.smi.VariableBinding; import org.snmp4j.transport.DefaultUdpTransportMapping; public class SnmpClient { public static String get(String address, int port, String oid) throws Exception { TransportMapping transport = new DefaultUdpTransportMapping(); Snmp snmp = new Snmp(transport); snmp.listen(); PDU pdu = new PDU(); pdu.add(new VariableBinding(new OID(oid))); pdu.setType(PDU.GET); CommunityTarget target = new CommunityTarget(); target.setCommunity(new OctetString("public")); target.setAddress(GenericAddress.parse(String.format("udp:%s/%s", address, port))); target.setRetries(2); target.setTimeout(1500); target.setVersion(SnmpConstants.version2c); ResponseEvent event = snmp.send(pdu, target); if (event != null) { return event.getResponse().get(0).getVariable().toString(); } return null; } }
Our method performs an SNMPv2c Get Request (highlighted) using a "public"
read community. The object identifier is placed in a variable binding. The response is then evaluated and returned. The above code should be easy to read and understand.
6. Run the Tests
Our test should pass now. The test output should look like the one below:
mvn test
------------------------------------------------------- T E S T S ------------------------------------------------------- Running com.javacodegeeks.example.SnmpClientTest Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.665 sec Results : Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 6.716 s [INFO] Finished at: 2017-07-03T20:56:04+08:00 [INFO] Final Memory: 10M/164M [INFO] ------------------------------------------------------------------------
7. Test-Driven Development with SNMP4J Summary
In the TDD approach, we wrote the unit test first. Ran the unit test to watch it fail. Then wrote the code to make it pass. We extended the BaseAgent
class of SNMP4J to create a test agent. We added a scalar object as our test data which will be requested by the client. Lastly, our production code communicated with the test agent.
8. Download the Source Code
This is an example of test-driven development with SNMP4J.
You can download the source code of this example here: snmp4j-unit-test.zip.