diff --git a/lib/formatters/junit.js b/lib/formatters/junit.js
new file mode 100644
index 000000000000..08fc45e8fdbe
--- /dev/null
+++ b/lib/formatters/junit.js
@@ -0,0 +1,95 @@
+/**
+ * @fileoverview jUnit Reporter
+ * @author Jamund Ferguson
+ */
+
+/*jshint node:true*/
+
+//------------------------------------------------------------------------------
+// Helper Functions
+//------------------------------------------------------------------------------
+
+function getMessageType(message, rules) {
+
+ if (message.fatal || rules[message.ruleId] === 2) {
+ return "Error";
+ } else {
+ return "Warning";
+ }
+
+}
+
+/**
+ * Replace special characters before write to output.
+ *
+ * Rules:
+ * - single quotes is the escape sequence for double-quotes
+ * - < is the escape sequence for <
+ * - > is the escape sequence for >
+ * - " is the escape sequence for "
+ * - ' is the escape sequence for '
+ * - & is the escape sequence for &
+ *
+ * @param {String} message to escape
+ * @return escaped message as {String}
+ */
+function escapeSpecialCharacters(str) {
+
+ str = str || "";
+ var pairs = {
+ "&": "&",
+ "\"": """,
+ "'": "'",
+ "<": "<",
+ ">": ">"
+ };
+ for (var r in pairs) {
+ str = str.replace(new RegExp(r, "g"), pairs[r]);
+ }
+ return str || "";
+
+}
+
+//------------------------------------------------------------------------------
+// Public Interface
+//------------------------------------------------------------------------------
+
+module.exports = function(results, config) {
+
+ var output = "",
+ rules = config.rules || {};
+
+ output += "\n";
+ output += "\n";
+
+ results.forEach(function(result) {
+
+ var messages = result.messages;
+
+ if (messages.length) {
+ output += "\n";
+ }
+
+ messages.forEach(function(message) {
+ var type = message.fatal ? "error" : "failure";
+ output += "";
+ output += "<" + type + " message=\"" + escapeSpecialCharacters(message.message) + "\">";
+ output += "";
+ output += "" + type + ">";
+ output += "\n";
+ });
+
+ if (messages.length) {
+ output += "\n";
+ }
+
+ });
+
+ output += "\n";
+
+ return output;
+};
\ No newline at end of file
diff --git a/tests/lib/formatters/junit.js b/tests/lib/formatters/junit.js
new file mode 100644
index 000000000000..e0102b874f7f
--- /dev/null
+++ b/tests/lib/formatters/junit.js
@@ -0,0 +1,136 @@
+/**
+ * @fileoverview Tests for jUnit Formatter.
+ * @author Jamund Ferguson
+ */
+
+/*jshint node:true*/
+
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+var vows = require("vows"),
+ assert = require("assert"),
+ sinon = require("sinon"),
+ formatter = require("../../../lib/formatters/junit");
+
+//------------------------------------------------------------------------------
+// Tests
+//------------------------------------------------------------------------------
+
+vows.describe("formatter:junit").addBatch({
+
+ "when there are no problems": {
+
+ topic: [],
+
+ "should not complain about anything": function(topic) {
+ var config = {}; // not needed for this test
+ var result = formatter(topic, config);
+ assert.equal('', result.replace(/\n/g, ""));
+ }
+ },
+
+ "when passed a single message": {
+
+ topic: [{
+ filePath: "foo.js",
+ messages: [{
+ message: "Unexpected foo.",
+ line: 5,
+ column: 10,
+ ruleId: "foo"
+ }]
+ }],
+
+ "should return a single with a message and the line and col number in the body (error)": function(topic) {
+ var config = {
+ rules: { foo: 2 }
+ };
+
+ var result = formatter(topic, config);
+ assert.equal('', result.replace(/\n/g, ""));
+ },
+
+ "should return a single with a message and the line and col number in the body (warning)": function(topic) {
+ var config = {
+ rules: { foo: 1 }
+ };
+
+ var result = formatter(topic, config);
+ assert.equal('', result.replace(/\n/g, ""));
+ }
+
+ },
+
+ "when passed a fatal error message": {
+
+ topic: [{
+ filePath: "foo.js",
+ messages: [{
+ fatal: true,
+ message: "Unexpected foo.",
+ line: 5,
+ column: 10,
+ ruleId: "foo"
+ }]
+ }],
+
+ "should return a single and an ": function(topic) {
+ var config = {};
+ var result = formatter(topic, config);
+ assert.equal('', result.replace(/\n/g, ""));
+ }
+ },
+
+ "when passed multiple messages": {
+
+ topic: [{
+ filePath: "foo.js",
+ messages: [{
+ message: "Unexpected foo.",
+ line: 5,
+ column: 10,
+ ruleId: "foo"
+ }, {
+ message: "Unexpected bar.",
+ line: 6,
+ column: 11,
+ ruleId: "bar"
+ }]
+ }],
+
+ "should return a multiple 's": function(topic) {
+ var config = {
+ rules: { foo: 2, bar: 1 }
+ };
+
+ var result = formatter(topic, config);
+ assert.equal('', result.replace(/\n/g, ""));
+ }
+
+ },
+
+ "when passed special characters": {
+
+ topic: [{
+ filePath: "foo.js",
+ messages: [{
+ message: "Unexpected .",
+ line: 5,
+ column: 10,
+ ruleId: "foo"
+ }]
+ }],
+
+ "should make them go away": function(topic) {
+ var config = {
+ rules: { foo: 1 }
+ };
+
+ var result = formatter(topic, config);
+ assert.equal('', result.replace(/\n/g, ""));
+
+ }
+ },
+}).export(module);