tests: introduce unit testing

As a sample, this commit also includes a test suite for
ndn::ping::client::StatisticsCollector class.

refs #2795

Change-Id: Ia0c7522ae26bee1cc9d172c7379ab62f84325ed7
diff --git a/core/common.hpp b/core/common.hpp
index 2b031cc..fa035c6 100644
--- a/core/common.hpp
+++ b/core/common.hpp
@@ -26,6 +26,18 @@
 #ifndef NDN_TOOLS_CORE_COMMON_HPP
 #define NDN_TOOLS_CORE_COMMON_HPP
 
+#ifdef WITH_TESTS
+#define VIRTUAL_WITH_TESTS virtual
+#define PUBLIC_WITH_TESTS_ELSE_PROTECTED public
+#define PUBLIC_WITH_TESTS_ELSE_PRIVATE public
+#define PROTECTED_WITH_TESTS_ELSE_PRIVATE protected
+#else
+#define VIRTUAL_WITH_TESTS
+#define PUBLIC_WITH_TESTS_ELSE_PROTECTED protected
+#define PUBLIC_WITH_TESTS_ELSE_PRIVATE private
+#define PROTECTED_WITH_TESTS_ELSE_PRIVATE private
+#endif
+
 #include <cinttypes>
 #include <cstddef>
 
diff --git a/tests/README.md b/tests/README.md
new file mode 100644
index 0000000..ddcd0c5
--- /dev/null
+++ b/tests/README.md
@@ -0,0 +1,23 @@
+ndn-tool unit tests
+===================
+
+## Assumptions
+
+Unit tests for a tool `foo` should be placed in the folder `foo` and build script for the tool
+should define `foo-objects` that includes all object files for the tool, except object files
+defining main function.
+
+For example,
+
+    bld(features='cxx',
+        name='tool-subtool-objects',
+        source=bld.path.ant_glob('subtool/*.cpp', excl='subtool/main.cpp'),
+        use='core-objects')
+
+    bld(features='cxx cxxprogram',
+        target='../../bin/subtool',
+        source='subtool/main.cpp',
+        use='tool-subtool-objects')
+
+    bld(name='tool-objects',
+        use='tool-subtool-objects')
diff --git a/tests/boost-test.hpp b/tests/boost-test.hpp
new file mode 100644
index 0000000..cd4ccfe
--- /dev/null
+++ b/tests/boost-test.hpp
@@ -0,0 +1,37 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/**
+ * Copyright (c) 2014-2015,  Regents of the University of California,
+ *                           Arizona Board of Regents,
+ *                           Colorado State University,
+ *                           University Pierre & Marie Curie, Sorbonne University,
+ *                           Washington University in St. Louis,
+ *                           Beijing Institute of Technology,
+ *                           The University of Memphis.
+ *
+ * This file is part of ndn-tools (Named Data Networking Essential Tools).
+ * See AUTHORS.md for complete list of ndn-tools authors and contributors.
+ *
+ * ndn-tools is free software: you can redistribute it and/or modify it under the terms
+ * of the GNU General Public License as published by the Free Software Foundation,
+ * either version 3 of the License, or (at your option) any later version.
+ *
+ * ndn-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE.  See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * ndn-tools, e.g., in COPYING.md file.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef NDN_TOOLS_TESTS_BOOST_TEST_HPP
+#define NDN_TOOLS_TESTS_BOOST_TEST_HPP
+
+// suppress warnings from Boost.Test
+#pragma GCC system_header
+#pragma clang system_header
+
+#include <boost/concept_check.hpp>
+#include <boost/test/unit_test.hpp>
+#include <boost/test/output_test_stream.hpp>
+
+#endif // NDN_TOOLS_TESTS_BOOST_TEST_HPP
diff --git a/tests/main.cpp b/tests/main.cpp
new file mode 100644
index 0000000..3a2a4fb
--- /dev/null
+++ b/tests/main.cpp
@@ -0,0 +1,29 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/**
+ * Copyright (c) 2014-2015,  Regents of the University of California,
+ *                           Arizona Board of Regents,
+ *                           Colorado State University,
+ *                           University Pierre & Marie Curie, Sorbonne University,
+ *                           Washington University in St. Louis,
+ *                           Beijing Institute of Technology,
+ *                           The University of Memphis.
+ *
+ * This file is part of ndn-tools (Named Data Networking Essential Tools).
+ * See AUTHORS.md for complete list of ndn-tools authors and contributors.
+ *
+ * ndn-tools is free software: you can redistribute it and/or modify it under the terms
+ * of the GNU General Public License as published by the Free Software Foundation,
+ * either version 3 of the License, or (at your option) any later version.
+ *
+ * ndn-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE.  See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * ndn-tools, e.g., in COPYING.md file.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#define BOOST_TEST_MAIN 1
+#define BOOST_TEST_DYN_LINK 1
+
+#include "boost-test.hpp"
diff --git a/tests/ping/client/statistics-collector.t.cpp b/tests/ping/client/statistics-collector.t.cpp
new file mode 100644
index 0000000..5dcf941
--- /dev/null
+++ b/tests/ping/client/statistics-collector.t.cpp
@@ -0,0 +1,148 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/**
+ * Copyright (c) 2014-2015,  Arizona Board of Regents.
+ *
+ * This file is part of ndn-tools (Named Data Networking Essential Tools).
+ * See AUTHORS.md for complete list of ndn-tools authors and contributors.
+ *
+ * ndn-tools is free software: you can redistribute it and/or modify it under the terms
+ * of the GNU General Public License as published by the Free Software Foundation,
+ * either version 3 of the License, or (at your option) any later version.
+ *
+ * ndn-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE.  See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * ndn-tools, e.g., in COPYING.md file.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "tools/ping/client/statistics-collector.hpp"
+#include <ndn-cxx/util/dummy-client-face.hpp>
+
+#include "tests/test-common.hpp"
+
+namespace ndn {
+namespace ping {
+namespace client {
+namespace tests {
+
+using namespace ndn::tests;
+
+class StatisticsCollectorFixture
+{
+protected:
+  StatisticsCollectorFixture()
+    : face(util::makeDummyClientFace())
+    , pingOptions(makeOptions())
+    , pingProgram(*face, pingOptions)
+    , sc(pingProgram, pingOptions)
+  {
+  }
+
+private:
+  static Options
+  makeOptions()
+  {
+    Options opt;
+    opt.prefix = "ndn:/ping-prefix";
+    opt.shouldAllowStaleData = false;
+    opt.shouldGenerateRandomSeq = false;
+    opt.shouldPrintTimestamp = false;
+    opt.nPings = 5;
+    opt.interval = time::milliseconds(100);
+    opt.timeout = time::milliseconds(2000);
+    opt.startSeq = 1;
+    return opt;
+  }
+
+protected:
+  shared_ptr<util::DummyClientFace> face;
+  Options pingOptions;
+  Ping pingProgram;
+  StatisticsCollector sc;
+};
+
+BOOST_FIXTURE_TEST_SUITE(PingClientStatisticsCollector, StatisticsCollectorFixture)
+
+BOOST_AUTO_TEST_CASE(Resp50msResp50ms)
+{
+  sc.recordResponse(time::milliseconds(50));
+
+  Statistics stats1 = sc.computeStatistics();
+  BOOST_CHECK_EQUAL(stats1.prefix, pingOptions.prefix);
+  BOOST_CHECK_EQUAL(stats1.nSent, 1);
+  BOOST_CHECK_EQUAL(stats1.nReceived, 1);
+  BOOST_CHECK_CLOSE(stats1.minRtt, 50.0, 0.001);
+  BOOST_CHECK_CLOSE(stats1.maxRtt, 50.0, 0.001);
+  BOOST_CHECK_CLOSE(stats1.packetLossRate, 0.0, 0.001);
+  BOOST_CHECK_CLOSE(stats1.sumRtt, 50.0, 0.001);
+  BOOST_CHECK_CLOSE(stats1.avgRtt, 50.0, 0.001);
+  BOOST_CHECK_CLOSE(stats1.stdDevRtt, 0.0, 0.001);
+
+  sc.recordResponse(time::milliseconds(50));
+
+  Statistics stats2 = sc.computeStatistics();
+  BOOST_CHECK_EQUAL(stats2.prefix, pingOptions.prefix);
+  BOOST_CHECK_EQUAL(stats2.nSent, 2);
+  BOOST_CHECK_EQUAL(stats2.nReceived, 2);
+  BOOST_CHECK_CLOSE(stats2.minRtt, 50.0, 0.001);
+  BOOST_CHECK_CLOSE(stats2.maxRtt, 50.0, 0.001);
+  BOOST_CHECK_CLOSE(stats2.packetLossRate, 0.0, 0.001);
+  BOOST_CHECK_CLOSE(stats2.sumRtt, 100.0, 0.001);
+  BOOST_CHECK_CLOSE(stats2.avgRtt, 50.0, 0.001);
+  BOOST_CHECK_CLOSE(stats2.stdDevRtt, 0.0, 0.001);
+}
+
+BOOST_AUTO_TEST_CASE(Resp50msResp100ms)
+{
+  sc.recordResponse(time::milliseconds(50));
+  sc.recordResponse(time::milliseconds(100));
+
+  Statistics stats = sc.computeStatistics();
+  BOOST_CHECK_EQUAL(stats.prefix, pingOptions.prefix);
+  BOOST_CHECK_EQUAL(stats.nSent, 2);
+  BOOST_CHECK_EQUAL(stats.nReceived, 2);
+  BOOST_CHECK_CLOSE(stats.minRtt, 50.0, 0.001);
+  BOOST_CHECK_CLOSE(stats.maxRtt, 100.0, 0.001);
+  BOOST_CHECK_CLOSE(stats.packetLossRate, 0.0, 0.001);
+  BOOST_CHECK_CLOSE(stats.sumRtt, 150.0, 0.001);
+  BOOST_CHECK_CLOSE(stats.avgRtt, 75.0, 0.001);
+  BOOST_CHECK_CLOSE(stats.stdDevRtt, 25.0, 0.001);
+}
+
+BOOST_AUTO_TEST_CASE(LossLoss)
+{
+  sc.recordTimeout();
+  sc.recordTimeout();
+
+  Statistics stats = sc.computeStatistics();
+  BOOST_CHECK_EQUAL(stats.prefix, pingOptions.prefix);
+  BOOST_CHECK_EQUAL(stats.nSent, 2);
+  BOOST_CHECK_EQUAL(stats.nReceived, 0);
+  BOOST_CHECK_CLOSE(stats.packetLossRate, 1.0, 0.001);
+}
+
+BOOST_AUTO_TEST_CASE(Resp50msLoss)
+{
+  sc.recordResponse(time::milliseconds(50));
+  sc.recordTimeout();
+
+  Statistics stats = sc.computeStatistics();
+  BOOST_CHECK_EQUAL(stats.prefix, pingOptions.prefix);
+  BOOST_CHECK_EQUAL(stats.nSent, 2);
+  BOOST_CHECK_EQUAL(stats.nReceived, 1);
+  BOOST_CHECK_CLOSE(stats.minRtt, 50.0, 0.001);
+  BOOST_CHECK_CLOSE(stats.maxRtt, 50.0, 0.001);
+  BOOST_CHECK_CLOSE(stats.packetLossRate, 0.5, 0.001);
+  BOOST_CHECK_CLOSE(stats.sumRtt, 50.0, 0.001);
+  BOOST_CHECK_CLOSE(stats.avgRtt, 50.0, 0.001);
+  BOOST_CHECK_CLOSE(stats.stdDevRtt, 0.0, 0.001);
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // namespace tests
+} // namespace client
+} // namespace ping
+} // namespace ndn
diff --git a/tests/test-case.t.cpp.sample b/tests/test-case.t.cpp.sample
new file mode 100644
index 0000000..27c6d8a
--- /dev/null
+++ b/tests/test-case.t.cpp.sample
@@ -0,0 +1,61 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/**
+ * Copyright (c) 2014-2015,  Arizona Board of Regents.
+ *
+ * This file is part of ndn-tools (Named Data Networking Essential Tools).
+ * See AUTHORS.md for complete list of ndn-tools authors and contributors.
+ *
+ * ndn-tools is free software: you can redistribute it and/or modify it under the terms
+ * of the GNU General Public License as published by the Free Software Foundation,
+ * either version 3 of the License, or (at your option) any later version.
+ *
+ * ndn-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE.  See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * ndn-tools, e.g., in COPYING.md file.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+// #include "unit-under-test.hpp"
+// Unit being tested MUST be included first, to ensure header compiles on its own.
+// For further information about test naming conventions, see
+// http://redmine.named-data.net/projects/nfd/wiki/UnitTesting
+
+#include "tests/test-common.hpp"
+
+namespace ndn {
+namespace tool_name {
+namespace tests {
+// Unit tests SHOULD go inside ndn::tool_name::tests namespace.
+
+// Common fixtures in ndn::tests can be imported.
+using namespace ndn::tests;
+
+// See http://redmine.named-data.net/projects/nfd/wiki/UnitTesting on how to name a test suite.
+BOOST_AUTO_TEST_SUITE(TestSkeleton)
+
+BOOST_AUTO_TEST_CASE(Test1)
+{
+  int i = 0;
+
+  // For reference of available Boost.Test macros, see
+  // http://www.boost.org/doc/libs/1_48_0/libs/test/doc/html/utf/testing-tools/reference.html
+
+  BOOST_REQUIRE_NO_THROW(i = 1);
+  BOOST_REQUIRE_EQUAL(i, 1);
+}
+
+// Use UnitTestTimeFixture to mock clocks.
+BOOST_FIXTURE_TEST_CASE(Test2, UnitTestTimeFixture)
+{
+  // this->advanceClocks increments mock clocks.
+  boost::asio::io_service io;
+  this->advanceClocks(io, time::milliseconds(500));
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // namespace tests
+} // namespace tool_name
+} // namespace ndn
diff --git a/tests/test-common.hpp b/tests/test-common.hpp
new file mode 100644
index 0000000..eaee1c6
--- /dev/null
+++ b/tests/test-common.hpp
@@ -0,0 +1,146 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/**
+ * Copyright (c) 2014-2015,  Regents of the University of California,
+ *                           Arizona Board of Regents,
+ *                           Colorado State University,
+ *                           University Pierre & Marie Curie, Sorbonne University,
+ *                           Washington University in St. Louis,
+ *                           Beijing Institute of Technology,
+ *                           The University of Memphis.
+ *
+ * This file is part of ndn-tools (Named Data Networking Essential Tools).
+ * See AUTHORS.md for complete list of ndn-tools authors and contributors.
+ *
+ * ndn-tools is free software: you can redistribute it and/or modify it under the terms
+ * of the GNU General Public License as published by the Free Software Foundation,
+ * either version 3 of the License, or (at your option) any later version.
+ *
+ * ndn-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE.  See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * ndn-tools, e.g., in COPYING.md file.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef NDN_TOOLS_TESTS_TEST_COMMON_HPP
+#define NDN_TOOLS_TESTS_TEST_COMMON_HPP
+
+#include "boost-test.hpp"
+
+#include <ndn-cxx/util/time-unit-test-clock.hpp>
+#include <ndn-cxx/name.hpp>
+#include <ndn-cxx/interest.hpp>
+#include <ndn-cxx/data.hpp>
+#include <ndn-cxx/signature.hpp>
+#include <ndn-cxx/security/signature-sha256-with-rsa.hpp>
+
+#include <boost/asio/io_service.hpp>
+
+namespace ndn {
+namespace tests {
+
+/** \brief a test fixture that overrides steady clock and system clock
+ */
+class UnitTestTimeFixture
+{
+protected:
+  UnitTestTimeFixture()
+    : steadyClock(make_shared<time::UnitTestSteadyClock>())
+    , systemClock(make_shared<time::UnitTestSystemClock>())
+  {
+    time::setCustomClocks(steadyClock, systemClock);
+  }
+
+  ~UnitTestTimeFixture()
+  {
+    time::setCustomClocks(nullptr, nullptr);
+  }
+
+  /** \brief advance steady and system clocks
+   *
+   *  Clocks are advanced in increments of \p tick for \p nTicks ticks.
+   *  After each tick, the supplied io_service is polled to process pending I/O events.
+   *
+   *  Exceptions thrown during I/O events are propagated to the caller.
+   *  Clock advancing would stop in case of an exception.
+   */
+  void
+  advanceClocks(boost::asio::io_service& io,
+                const time::nanoseconds& tick, size_t nTicks = 1)
+  {
+    BOOST_ASSERT(nTicks >= 0);
+
+    this->advanceClocks(io, tick, tick * nTicks);
+  }
+
+  /** \brief advance steady and system clocks
+   *
+   *  Clocks are advanced in increments of \p tick for \p total time.
+   *  The last increment might be shorter than \p tick.
+   *  After each tick, the supplied io_service is polled to process pending I/O events.
+   *
+   *  Exceptions thrown during I/O events are propagated to the caller.
+   *  Clock advancing would stop in case of an exception.
+   */
+  void
+  advanceClocks(boost::asio::io_service& io,
+                const time::nanoseconds& tick, const time::nanoseconds& total)
+  {
+    BOOST_ASSERT(tick > time::nanoseconds::zero());
+    BOOST_ASSERT(total >= time::nanoseconds::zero());
+
+    time::nanoseconds remaining = total;
+    while (remaining > time::nanoseconds::zero()) {
+      if (remaining >= tick) {
+        steadyClock->advance(tick);
+        systemClock->advance(tick);
+        remaining -= tick;
+      }
+      else {
+        steadyClock->advance(remaining);
+        systemClock->advance(remaining);
+        remaining = time::nanoseconds::zero();
+      }
+
+      if (io.stopped())
+        io.reset();
+      io.poll();
+    }
+  }
+
+protected:
+  shared_ptr<time::UnitTestSteadyClock> steadyClock;
+  shared_ptr<time::UnitTestSystemClock> systemClock;
+};
+
+inline shared_ptr<Interest>
+makeInterest(const Name& name)
+{
+  return make_shared<Interest>(name);
+}
+
+inline shared_ptr<Data>
+signData(const shared_ptr<Data>& data)
+{
+  ndn::SignatureSha256WithRsa fakeSignature;
+  fakeSignature.setValue(ndn::dataBlock(tlv::SignatureValue,
+                                        static_cast<const uint8_t*>(nullptr), 0));
+  data->setSignature(fakeSignature);
+  data->wireEncode();
+
+  return data;
+}
+
+inline shared_ptr<Data>
+makeData(const Name& name)
+{
+  shared_ptr<Data> data = make_shared<Data>(name);
+  return signData(data);
+}
+
+
+} // namespace tests
+} // namespace ndn
+
+#endif // NDN_TOOLS_TESTS_TEST_COMMON_HPP
diff --git a/tests/wscript b/tests/wscript
new file mode 100644
index 0000000..6066890
--- /dev/null
+++ b/tests/wscript
@@ -0,0 +1,15 @@
+# -*- Mode: python; py-indent-offset: 4; indent-tabs-mode: nil; coding: utf-8; -*-
+top = '..'
+
+def build(bld):
+    if not bld.env['WITH_TESTS']:
+        return
+
+    tools = bld.path.ant_glob('*', dir=True, src=False)
+
+    bld(target='../unit-tests',
+        features='cxx cxxprogram',
+        source=bld.path.ant_glob('**/*.cpp'),
+        use=['core-objects'] + ['%s-objects' % tool for tool in tools],
+        headers='../common.hpp boost-test.hpp',
+        )
diff --git a/tools/dump/wscript b/tools/dump/wscript
index 28e58b6..32b3695 100644
--- a/tools/dump/wscript
+++ b/tools/dump/wscript
@@ -33,10 +33,15 @@
                    uselib_store='PCAP', mandatory=True)
 
 def build(bld):
-    bld.program(
-        features='cxx',
-        target='../../bin/ndndump',
-        source=bld.path.ant_glob(['*.cpp']),
+    bld(features='cxx',
+        name='dump-objects',
+        source=bld.path.ant_glob('*.cpp', excl='main.cpp'),
         includes='.',
+        export_includes='.',
         use='core-objects BOOST PCAP',
         )
+
+    bld(features='cxx cxxprogram',
+        target='../../bin/ndndump',
+        source='main.cpp',
+        use='dump-objects')
diff --git a/tools/ping/client/statistics-collector.hpp b/tools/ping/client/statistics-collector.hpp
index 685ff1f..76278db 100644
--- a/tools/ping/client/statistics-collector.hpp
+++ b/tools/ping/client/statistics-collector.hpp
@@ -64,6 +64,13 @@
   StatisticsCollector(Ping& ping, const Options& options);
 
   /**
+   * Returns ping statistics as structure
+   */
+  Statistics
+  computeStatistics();
+
+PUBLIC_WITH_TESTS_ELSE_PRIVATE:
+  /**
    * Called on ping response received
    * @param rtt round trip time
    */
@@ -76,12 +83,6 @@
   void
   recordTimeout();
 
-  /**
-   * Returns ping statistics as structure
-   */
-  Statistics
-  computeStatistics();
-
 private:
   Ping& m_ping;
   const Options& m_options;
diff --git a/tools/ping/wscript b/tools/ping/wscript
index 575e395..3e6755d 100644
--- a/tools/ping/wscript
+++ b/tools/ping/wscript
@@ -2,16 +2,28 @@
 top = '../..'
 
 def build(bld):
-    bld.program(
-        features='cxx',
-        target='../../bin/ndnping',
-        source=bld.path.ant_glob('client/*.cpp'),
-        use='core-objects',
-        )
 
-    bld.program(
-        features='cxx',
+    bld(features='cxx',
+        name='ping-client-objects',
+        source=bld.path.ant_glob('client/*.cpp', excl='client/ndn-ping.cpp'),
+        use='core-objects')
+
+    bld(features='cxx cxxprogram',
+        target='../../bin/ndnping',
+        source='client/ndn-ping.cpp',
+        use='ping-client-objects')
+
+    bld(features='cxx',
+        name='ping-server-objects',
+        source=bld.path.ant_glob('server/*.cpp', excl='server/ndn-ping-server.cpp'),
+        use='core-objects')
+
+    bld(features='cxx cxxprogram',
         target='../../bin/ndnpingserver',
-        source=bld.path.ant_glob('server/*.cpp'),
-        use='core-objects',
-        )
+        source='server/ndn-ping-server.cpp',
+        use='ping-server-objects')
+
+    ## (for unit tests)
+
+    bld(name='ping-objects',
+        use='ping-client-objects ping-server-objects')
diff --git a/wscript b/wscript
index 0b5aee1..e7dd99b 100644
--- a/wscript
+++ b/wscript
@@ -7,6 +7,8 @@
 def options(opt):
     opt.load(['compiler_cxx', 'gnu_dirs'])
     opt.load(['default-compiler-flags', 'sphinx_build', 'boost'], tooldir=['.waf-tools'])
+    opt.add_option('--with-tests', action='store_true', default=False,
+                   dest='with_tests', help='''Build unit tests''')
 
 def configure(conf):
     conf.load(['compiler_cxx', 'gnu_dirs',
@@ -20,7 +22,12 @@
     conf.check_cfg(package='libndn-cxx', args=['--cflags', '--libs'],
                    uselib_store='NDN_CXX', mandatory=True)
 
-    conf.check_boost(lib='system iostreams regex')
+    boost_libs = 'system iostreams regex'
+    if conf.options.with_tests:
+        conf.env['WITH_TESTS'] = 1
+        conf.define('WITH_TESTS', 1);
+        boost_libs += ' unit_test_framework'
+    conf.check_boost(lib=boost_libs)
 
     conf.recurse('tools')
 
@@ -32,9 +39,10 @@
         name='core-objects',
         features='cxx',
         source=bld.path.ant_glob(['core/*.cpp']),
-        use='NDN_CXX',
+        use='NDN_CXX BOOST',
         export_includes='.',
         )
 
     bld.recurse('tools')
+    bld.recurse('tests')
     bld.recurse('manpages')