diff --git a/tests/src/actionContainers/ActionContainer.scala b/tests/src/actionContainers/ActionContainer.scala
index abcc72c34479337073ef483128ea7b289d842f90..c5350a3d4c3f90759bec1362c7f0bf284e01109f 100644
--- a/tests/src/actionContainers/ActionContainer.scala
+++ b/tests/src/actionContainers/ActionContainer.scala
@@ -13,7 +13,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package actionContainers
 
 import java.io.ByteArrayOutputStream
diff --git a/tests/src/actionContainers/JavaActionContainerTests.scala b/tests/src/actionContainers/JavaActionContainerTests.scala
index 54b8da86cf6575a6cd4090d3fcf0ac97bb878466..77bfff7a1efe2f40eca1713195502e026c9c7287 100644
--- a/tests/src/actionContainers/JavaActionContainerTests.scala
+++ b/tests/src/actionContainers/JavaActionContainerTests.scala
@@ -23,9 +23,9 @@ import org.scalatest.junit.JUnitRunner
 import spray.json._
 
 import ActionContainer.withContainer
-import common.WskActorSystem
+import ResourceHelpers.JarBuilder
 
-import collection.JavaConverters._
+import common.WskActorSystem
 
 @RunWith(classOf[JUnitRunner])
 class JavaActionContainerTests extends FlatSpec with Matchers with WskActorSystem {
@@ -273,134 +273,3 @@ class JavaActionContainerTests extends FlatSpec with Matchers with WskActorSyste
         classLoaderTest("thread")
     }
 }
-
-/**
- * A convenience object to compile and package Java sources into a JAR, and to
- * encode that JAR as a base 64 string. The compilation options include the
- * current classpath, which is why Google GSON is readily available (though not
- * packaged in the JAR).
- */
-object JarBuilder {
-    import java.net.URI
-    import java.net.URLClassLoader
-    import java.nio.file.Files
-    import java.nio.file.Path
-    import java.nio.file.Paths
-    import java.nio.file.SimpleFileVisitor
-    import java.nio.file.FileVisitResult
-    import java.nio.file.FileSystems
-    import java.nio.file.attribute.BasicFileAttributes
-    import java.nio.charset.StandardCharsets
-    import java.util.Base64
-
-    import javax.tools.ToolProvider
-
-    def mkBase64Jar(sources: Seq[(Seq[String], String)]): String = {
-        // Note that this pipeline doesn't delete any of the temporary files.
-        val binDir = compile(sources)
-        val jarPath = makeJar(binDir)
-        val base64 = toBase64(jarPath)
-        base64
-    }
-
-    def mkBase64Jar(source: (Seq[String], String)): String = {
-        mkBase64Jar(Seq(source))
-    }
-
-    private def compile(sources: Seq[(Seq[String], String)]): Path = {
-        require(!sources.isEmpty)
-
-        // A temporary directory for the source files.
-        val srcDir = Files.createTempDirectory("src").toAbsolutePath()
-
-        // The absolute paths of the source file
-        val srcAbsPaths = for ((sourceName, sourceContent) <- sources) yield {
-            // The relative path of the source file
-            val srcRelPath = Paths.get(sourceName.head, sourceName.tail: _*)
-            // The absolute path of the source file
-            val srcAbsPath = srcDir.resolve(srcRelPath)
-            // Create parent directories if needed.
-            Files.createDirectories(srcAbsPath.getParent)
-            // Writing contents
-            Files.write(srcAbsPath, sourceContent.getBytes(StandardCharsets.UTF_8))
-
-            srcAbsPath
-        }
-
-        // A temporary directory for the destination files.
-        val binDir = Files.createTempDirectory("bin").toAbsolutePath()
-
-        // Preparing the compiler
-        val compiler = ToolProvider.getSystemJavaCompiler()
-        val fileManager = compiler.getStandardFileManager(null, null, StandardCharsets.UTF_8)
-
-        // Collecting all files to be compiled
-        val compUnit = fileManager.getJavaFileObjectsFromFiles(srcAbsPaths.map(_.toFile).asJava)
-
-        // Setting the options
-        val compOptions = Seq(
-            "-d", binDir.toAbsolutePath().toString(),
-            "-classpath", buildClassPath())
-        val compTask = compiler.getTask(null, fileManager, null, compOptions.asJava, null, compUnit)
-
-        // ...and off we go.
-        compTask.call()
-
-        binDir
-    }
-
-    private def buildClassPath(): String = {
-        val bcp = System.getProperty("java.class.path")
-
-        val list = this.getClass().getClassLoader() match {
-            case ucl: URLClassLoader =>
-                bcp :: ucl.getURLs().map(_.getFile().toString()).toList
-
-            case _ =>
-                List(bcp)
-        }
-
-        list.mkString(System.getProperty("path.separator"))
-    }
-
-    private def makeJar(binDir: Path): Path = {
-        // Any temporary file name for the jar.
-        val jarPath = Files.createTempFile("output", ".jar").toAbsolutePath()
-        val jarUri = new URI("jar:" + jarPath.toUri().getScheme(), jarPath.toAbsolutePath().toString(), null)
-
-        // OK, that's a hack. Doing this because newFileSystem wants to create that file.
-        jarPath.toFile().delete()
-
-        // We "mount" it as a zip filesystem, so we can just copy files to it.
-        val fs = FileSystems.newFileSystem(jarUri, Map(("create" -> "true")).asJava)
-
-        // Traversing all files in the bin directory...
-        Files.walkFileTree(binDir, new SimpleFileVisitor[Path]() {
-            override def visitFile(path: Path, attributes: BasicFileAttributes) = {
-                // The path relative to the bin dir
-                val relPath = binDir.relativize(path)
-                // The corresponding path in the jar
-                val jarRelPath = fs.getPath(relPath.toString())
-
-                // Creating the directory structure if it doesn't exist.
-                if (!Files.exists(jarRelPath.getParent())) {
-                    Files.createDirectories(jarRelPath.getParent())
-                }
-
-                // Finally we can copy that file.
-                Files.copy(path, jarRelPath)
-
-                FileVisitResult.CONTINUE
-            }
-        })
-
-        fs.close()
-
-        jarPath
-    }
-
-    private def toBase64(path: Path): String = {
-        val encoder = Base64.getEncoder()
-        new String(encoder.encode(Files.readAllBytes(path)), StandardCharsets.UTF_8)
-    }
-}
diff --git a/tests/src/actionContainers/NodeJs6ActionContainerTests.scala b/tests/src/actionContainers/NodeJs6ActionContainerTests.scala
index 212d18dcecfc67208f83243e0edc562770bee12d..7b7992f838d196c4bb76bf670e13d791585d0d2d 100644
--- a/tests/src/actionContainers/NodeJs6ActionContainerTests.scala
+++ b/tests/src/actionContainers/NodeJs6ActionContainerTests.scala
@@ -16,6 +16,8 @@
 
 package actionContainers
 
+import whisk.core.entity.NodeJS6Exec
+
 import org.junit.runner.RunWith
 import org.scalatest.junit.JUnitRunner
 
@@ -27,6 +29,8 @@ class NodeJs6ActionContainerTests extends NodeJsActionContainerTests {
 
     override lazy val nodejsContainerImageName = "nodejs6action"
 
+    override def exec(code: String) = NodeJS6Exec(code, None)
+
     behavior of nodejsContainerImageName
 
     it should "support default function parameters" in {
diff --git a/tests/src/actionContainers/NodeJsActionContainerTests.scala b/tests/src/actionContainers/NodeJsActionContainerTests.scala
index 475b8e59e4ca07757bd9fb4e29135d1759945796..9844185d4042f712f072e5626aa7c807a0ca4ffd 100644
--- a/tests/src/actionContainers/NodeJsActionContainerTests.scala
+++ b/tests/src/actionContainers/NodeJsActionContainerTests.scala
@@ -18,7 +18,11 @@ package actionContainers
 import org.junit.runner.RunWith
 import org.scalatest.junit.JUnitRunner
 
+import whisk.core.entity.{ NodeJSAbstractExec, NodeJSExec }
+
 import ActionContainer.withContainer
+import ResourceHelpers.ZipBuilder
+
 import common.WskActorSystem
 import spray.json._
 
@@ -33,11 +37,17 @@ class NodeJsActionContainerTests extends BasicActionRunnerTests with WskActorSys
 
     def withNodeJsContainer(code: ActionContainer => Unit) = withActionContainer()(code)
 
-    override def initPayload(code: String) = JsObject(
-        "value" -> JsObject(
-            "name" -> JsString("dummyAction"),
-            "code" -> JsString(code),
-            "main" -> JsString("main")))
+    def exec(code: String): NodeJSAbstractExec = NodeJSExec(code, None)
+
+    override def initPayload(code: String) = {
+        val e = exec(code)
+        JsObject(
+            "value" -> JsObject(
+                "name" -> JsString("dummyAction"),
+                "code" -> JsString(e.code),
+                "binary" -> JsBoolean(e.binary),
+                "main" -> JsString("main")))
+    }
 
     behavior of nodejsContainerImageName
 
@@ -434,4 +444,79 @@ class NodeJsActionContainerTests extends BasicActionRunnerTests with WskActorSys
                 e shouldBe empty
         })
     }
+
+    it should "support zip-encoded npm package actions" in {
+        val srcs = Seq(
+            Seq("package.json") -> """
+                | {
+                |   "name": "wskaction",
+                |   "version": "1.0.0",
+                |   "description": "An OpenWhisk action as an npm package.",
+                |   "main": "index.js",
+                |   "author": "info@openwhisk.org",
+                |   "license": "Apache-2.0"
+                | }
+            """.stripMargin,
+            Seq("index.js") -> """
+                | exports.main = function (args) {
+                |     var name = typeof args["name"] === "string" ? args["name"] : "stranger";
+                |
+                |     return {
+                |         greeting: "Hello " + name + ", from an npm package action."
+                |     };
+                | }
+            """.stripMargin)
+
+        val code = ZipBuilder.mkBase64Zip(srcs)
+
+        val (out, err) = withNodeJsContainer { c =>
+            c.init(initPayload(code))._1 should be(200)
+
+            val (runCode, runRes) = c.run(runPayload(JsObject()))
+
+            runCode should be(200)
+            runRes.get.fields.get("greeting") shouldBe Some(JsString("Hello stranger, from an npm package action."))
+        }
+
+        checkStreams(out, err, {
+            case (o, e) =>
+                o shouldBe empty
+                e shouldBe empty
+        })
+    }
+
+    it should "fail gracefully on invalid zip files" in {
+        // Some text-file encoded to base64.
+        val code = "Q2VjaSBuJ2VzdCBwYXMgdW4gemlwLgo="
+
+        val (out, err) = withNodeJsContainer { c =>
+            c.init(initPayload(code))._1 should not be (200)
+        }
+
+        // Somewhere, the logs should mention the connection to the archive.
+        checkStreams(out, err, {
+            case (o, e) =>
+                (o + e).toLowerCase should include("error")
+                (o + e).toLowerCase should include("uncompressing")
+        })
+    }
+
+    it should "fail gracefully on valid zip files that are not actions" in {
+        val srcs = Seq(
+            Seq("hello") -> """
+                | Hello world!
+            """.stripMargin)
+
+        val code = ZipBuilder.mkBase64Zip(srcs)
+
+        val (out, err) = withNodeJsContainer { c =>
+            c.init(initPayload(code))._1 should not be (200)
+        }
+
+        checkStreams(out, err, {
+            case (o, e) =>
+                (o + e).toLowerCase should include("error")
+                (o + e).toLowerCase should include("module_not_found")
+        })
+    }
 }
diff --git a/tests/src/actionContainers/ResourceHelpers.scala b/tests/src/actionContainers/ResourceHelpers.scala
new file mode 100644
index 0000000000000000000000000000000000000000..b74bfbcaa14f40e3f69bc909db795eb8bd2e641b
--- /dev/null
+++ b/tests/src/actionContainers/ResourceHelpers.scala
@@ -0,0 +1,187 @@
+/*
+ * Copyright 2015-2016 IBM Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package actionContainers
+
+import java.net.URI
+import java.net.URLClassLoader
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.Paths
+import java.nio.file.SimpleFileVisitor
+import java.nio.file.FileVisitResult
+import java.nio.file.FileSystems
+import java.nio.file.attribute.BasicFileAttributes
+import java.nio.charset.StandardCharsets
+import java.util.Base64
+
+import javax.tools.ToolProvider
+
+import collection.JavaConverters._
+
+/**
+ * A collection of utility objects to create ephemeral action resources based
+ *  on file contents.
+ */
+object ResourceHelpers {
+    /** Creates a zip file based on the contents of a top-level directory. */
+    object ZipBuilder {
+        def mkBase64Zip(sources: Seq[(Seq[String], String)]): String = {
+            val (tmpDir, _) = writeSourcesToTempDirectory(sources)
+            val archive = makeZipFromDir(tmpDir)
+            readAsBase64(archive)
+        }
+    }
+
+    /**
+     * A convenience object to compile and package Java sources into a JAR, and to
+     * encode that JAR as a base 64 string. The compilation options include the
+     * current classpath, which is why Google GSON is readily available (though not
+     * packaged in the JAR).
+     */
+    object JarBuilder {
+        def mkBase64Jar(sources: Seq[(Seq[String], String)]): String = {
+            // Note that this pipeline doesn't delete any of the temporary files.
+            val binDir = compile(sources)
+            val jarPath = makeJarFromDir(binDir)
+            val base64 = readAsBase64(jarPath)
+            base64
+        }
+
+        def mkBase64Jar(source: (Seq[String], String)): String = {
+            mkBase64Jar(Seq(source))
+        }
+
+        private def compile(sources: Seq[(Seq[String], String)]): Path = {
+            require(!sources.isEmpty)
+
+            // The absolute paths of the source file
+            val (srcDir, srcAbsPaths) = writeSourcesToTempDirectory(sources)
+
+            // A temporary directory for the destination files.
+            val binDir = Files.createTempDirectory("bin").toAbsolutePath()
+
+            // Preparing the compiler
+            val compiler = ToolProvider.getSystemJavaCompiler()
+            val fileManager = compiler.getStandardFileManager(null, null, StandardCharsets.UTF_8)
+
+            // Collecting all files to be compiled
+            val compUnit = fileManager.getJavaFileObjectsFromFiles(srcAbsPaths.map(_.toFile).asJava)
+
+            // Setting the options
+            val compOptions = Seq(
+                "-d", binDir.toAbsolutePath().toString(),
+                "-classpath", buildClassPath())
+            val compTask = compiler.getTask(null, fileManager, null, compOptions.asJava, null, compUnit)
+
+            // ...and off we go.
+            compTask.call()
+
+            binDir
+        }
+
+        private def buildClassPath(): String = {
+            val bcp = System.getProperty("java.class.path")
+
+            val list = this.getClass().getClassLoader() match {
+                case ucl: URLClassLoader =>
+                    bcp :: ucl.getURLs().map(_.getFile().toString()).toList
+
+                case _ =>
+                    List(bcp)
+            }
+
+            list.mkString(System.getProperty("path.separator"))
+        }
+    }
+
+    /**
+     * Creates a temporary directory and reproduces the desired file structure
+     * in it. Returns the path of the temporary directory and the path of each
+     * file as represented in it.
+     */
+    private def writeSourcesToTempDirectory(sources: Seq[(Seq[String], String)]): (Path, Seq[Path]) = {
+        // A temporary directory for the source files.
+        val srcDir = Files.createTempDirectory("src").toAbsolutePath()
+
+        val srcAbsPaths = for ((sourceName, sourceContent) <- sources) yield {
+            // The relative path of the source file
+            val srcRelPath = Paths.get(sourceName.head, sourceName.tail: _*)
+            // The absolute path of the source file
+            val srcAbsPath = srcDir.resolve(srcRelPath)
+            // Create parent directories if needed.
+            Files.createDirectories(srcAbsPath.getParent)
+            // Writing contents
+            Files.write(srcAbsPath, sourceContent.getBytes(StandardCharsets.UTF_8))
+
+            srcAbsPath
+        }
+
+        (srcDir, srcAbsPaths)
+    }
+
+    private def makeZipFromDir(dir: Path): Path = makeArchiveFromDir(dir, ".zip")
+
+    private def makeJarFromDir(dir: Path): Path = makeArchiveFromDir(dir, ".jar")
+
+    /**
+     * Compresses all files beyond a directory into a zip file.
+     * Note that Jar files are just zip files.
+     */
+    private def makeArchiveFromDir(dir: Path, extension: String): Path = {
+        // Any temporary file name for the archive.
+        val arPath = Files.createTempFile("output", extension).toAbsolutePath()
+
+        // We "mount" it as a filesystem, so we can just copy files into it.
+        val dstUri = new URI("jar:" + arPath.toUri().getScheme(), arPath.toAbsolutePath().toString(), null)
+        // OK, that's a hack. Doing this because newFileSystem wants to create that file.
+        arPath.toFile().delete()
+        val fs = FileSystems.newFileSystem(dstUri, Map(("create" -> "true")).asJava)
+
+        // Traversing all files in the bin directory...
+        Files.walkFileTree(dir, new SimpleFileVisitor[Path]() {
+            override def visitFile(path: Path, attributes: BasicFileAttributes) = {
+                // The path relative to the src dir
+                val relPath = dir.relativize(path)
+
+                // The corresponding path in the zip
+                val arRelPath = fs.getPath(relPath.toString())
+
+                // If this file is not top-level in the src dir...
+                if (relPath.getParent() != null) {
+                    // ...create the directory structure if it doesn't exist.
+                    if (!Files.exists(arRelPath.getParent())) {
+                        Files.createDirectories(arRelPath.getParent())
+                    }
+                }
+
+                // Finally we can copy that file.
+                Files.copy(path, arRelPath)
+
+                FileVisitResult.CONTINUE
+            }
+        })
+
+        fs.close()
+
+        arPath
+    }
+
+    /** Reads the contents of a (possibly binary) file into a base64-encoded String */
+    private def readAsBase64(path: Path): String = {
+        val encoder = Base64.getEncoder()
+        new String(encoder.encode(Files.readAllBytes(path)), StandardCharsets.UTF_8)
+    }
+}