diff --git a/doc/grilo/plugins-testing.xml b/doc/grilo/plugins-testing.xml
index 063eeeb..7bea521 100644
--- a/doc/grilo/plugins-testing.xml
+++ b/doc/grilo/plugins-testing.xml
@@ -96,4 +96,103 @@ $ export GRL_DEBUG="registry:*"
     Available metadata obtained for the selected item will be shown in the
     right pane for users to inspect.
   </para>
+
+  <section>
+    <title>Simulating Network Replies</title>
+
+    <para>
+      For for offline testing of plug-ins, especially in automated tests it is
+      useful to simulate and reply predefined network replies. Therefore Grilo
+      provides a few facilities for mocking network replies.
+    </para>
+
+    <para>
+      To enable mocking set the environment variable GRL_NET_MOCKED. The value
+      of this variable is interpreted as colon separated list of configuration
+      settings, e.g. <code>config=mock-my-plugin.ini:throttle=0</code>
+
+      <itemizedlist>
+        <listitem>
+          <varname>config</varname> selects a different file than
+          "grl-mock-data.ini" for configuring mock answers.
+        </listitem>
+
+        <listitem>
+          <varname>throttle</varname> overrides any throttling requests from
+          plugins. Set this variable to zero to disable any throttling.
+        </listitem>
+      </itemizedlist>
+    </para>
+
+    <para>
+      For configuring mock answers, a simple .ini file is used. The file is
+      split into a "default" section and one section per URL.
+    </para>
+
+    <programlisting>
+[default]
+version = 1
+ignored-parameters = field1[,field2[,...]] or "*"
+
+[http://www.example.com]
+data = content/of/response.txt
+timeout = 500
+    </programlisting>
+
+    <para>
+      An easy way to capture the responses is to run your application with the
+      environment variable GRL_NET_CAPTURE_DIR. GrlNetWc will then write all
+      each response into a file following the pattern "url-timestamp". If the
+      directory does not exist yet it will be created.
+    </para>
+
+    <section>
+      <title>The Default Section</title>
+
+      <para>
+        This section needs to be present in any mock reply configuration file.
+
+        <itemizedlist>
+          <listitem>
+            <varname>version</varname> needs to be "1".
+          </listitem>
+
+          <listitem>
+            <varname>ignored-parameters</varname> is a comma separated list of
+            query parameter names that can be used to map URLs to sections
+            without paying attention to query parameters listed here.
+            By setting a value of "api_key" a request for
+            <code>http://www.example.com?q=test+query&amp;api_key=dummy</code>
+            will be answered with the mock data for
+            <code>http://www.example.com?q=test+query</code>.
+            Setting "api_key,q" or "*" will result in mock answer for
+            <code>http://www.example.com</code>.
+          </listitem>
+        </itemizedlist>
+      </para>
+    </section>
+
+    <section>
+      <title>The URL Sections</title>
+
+      <para>
+        The section title is used to map URLs to response files.
+
+        <itemizedlist>
+          <listitem>
+            <varname>data</varname> is a path to a text file containing the
+            raw response of the websserver. The path may be relative to this
+            configuration file or an absolute path.
+          </listitem>
+
+          <listitem>
+            <varname>timeout</varname> may be used to delay the response in
+            seconds. The default is to not delay at all.
+          </listitem>
+        </itemizedlist>
+
+        Skip the <varname>data</varname> field to provoke a "not found" error.
+      </para>
+    </section>
+  </section>
 </section>
diff --git a/libs/net/Makefile.am b/libs/net/Makefile.am
index fac5c9d..d70fdaa 100644
--- a/libs/net/Makefile.am
+++ b/libs/net/Makefile.am
@@ -13,7 +13,8 @@ libgrlnet_@GRL_MAJORMINOR@_la_DEPENDENCIES =	\
 
 libgrlnet_@GRL_MAJORMINOR@_la_SOURCES = \
 	grl-net-private.c		\
-	grl-net-wc.c
+	grl-net-wc.c		\
+	grl-net-mock.c
 
 libgrlnet_@GRL_MAJORMINOR@_la_CFLAGS =	\
 	-I $(top_srcdir)/src		\
diff --git a/libs/net/grl-net-mock-private.h b/libs/net/grl-net-mock-private.h
new file mode 100644
index 0000000..24ce249
--- /dev/null
+++ b/libs/net/grl-net-mock-private.h
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2012 Openismus GmbH
+ *
+ * Authors: Jens Georg <jensg@openismus.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA
+ *
+ */
+
+#ifndef _GRL_NET_MOCK_PRIVATE_H_
+#define _GRL_NET_MOCK_PRIVATE_H_
+
+#include "grl-net-wc.h"
+
+#define GRL_NET_MOCKED_VAR "GRL_NET_MOCKED"
+
+G_GNUC_INTERNAL
+gboolean is_mocked (void);
+
+G_GNUC_INTERNAL
+gboolean override_throttling (guint *throttling);
+
+G_GNUC_INTERNAL
+void get_url_mocked (GrlNetWc *self,
+                     const char *url,
+                     GHashTable *headers,
+                     GAsyncResult *result,
+                     GCancellable *cancellable);
+
+G_GNUC_INTERNAL
+void get_content_mocked (GrlNetWc *self,
+                         void *op,
+                         gchar **content,
+                         gsize *length);
+
+G_GNUC_INTERNAL
+void init_mock_requester (GrlNetWc *self);
+
+G_GNUC_INTERNAL
+void finalize_mock_requester (GrlNetWc *self);
+
+G_GNUC_INTERNAL
+void free_mock_op_res (void *op);
+
+#endif /* _GRL_NET_MOCK_PRIVATE_H_ */
diff --git a/libs/net/grl-net-mock.c b/libs/net/grl-net-mock.c
new file mode 100644
index 0000000..c878903
--- /dev/null
+++ b/libs/net/grl-net-mock.c
@@ -0,0 +1,300 @@
+/*
+ * Copyright (C) 2012 Openismus GmbH
+ *
+ * Authors: Jens Georg <jensg@openismus.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <glib/gstdio.h>
+#include <gio/gio.h>
+#include <libsoup/soup.h>
+#include <string.h>
+
+#define _GRILO_H_INSIDE_
+#include <grl-log.h>
+
+#include "grl-net-mock-private.h"
+
+#define GRL_MOCK_VERSION 1
+
+static GKeyFile *config = NULL;
+static GRegex *ignored_parameters = NULL;
+static char *base_path = NULL;
+static gboolean enable_mocking = FALSE;
+static guint throttle_override = G_MAXUINT;
+
+gboolean
+is_mocked (void)
+{
+  return enable_mocking;
+}
+
+gboolean
+override_throttling (guint *throttling)
+{
+  if (throttle_override == G_MAXUINT)
+    return FALSE;
+
+  *throttling = throttle_override;
+  return TRUE;
+}
+
+void
+get_url_mocked (GrlNetWc *self,
+                const char *url,
+                GHashTable *headers,
+                GAsyncResult *result,
+                GCancellable *cancellable)
+{
+  char *data_file, *full_path;
+  GError *error = NULL;
+  GStatBuf stat_buf;
+  char *new_url;
+
+  if (ignored_parameters) {
+    SoupURI *uri = soup_uri_new (url);
+    char *new_query = g_regex_replace (ignored_parameters,
+                                       soup_uri_get_query (uri), -1, 0,
+                                       "", 0, NULL);
+    soup_uri_set_query (uri, *new_query ? new_query : NULL);
+    new_url = soup_uri_to_string (uri, FALSE);
+    soup_uri_free (uri);
+  } else {
+    new_url = g_strdup (url);
+  }
+
+  if (!config) {
+    g_simple_async_result_set_error (G_SIMPLE_ASYNC_RESULT (result),
+                                     GRL_NET_WC_ERROR,
+                                     GRL_NET_WC_ERROR_NETWORK_ERROR,
+                                     "%s",
+                                     "No mock definition found");
+    g_simple_async_result_complete_in_idle (G_SIMPLE_ASYNC_RESULT (result));
+    return;
+  }
+
+  data_file = g_key_file_get_value (config, new_url, "data", &error);
+  if (error) {
+    g_simple_async_result_set_error (G_SIMPLE_ASYNC_RESULT (result),
+                                     GRL_NET_WC_ERROR,
+                                     GRL_NET_WC_ERROR_NOT_FOUND,
+                                     "Could not find mock content: %s",
+                                     error->message);
+    g_error_free (error);
+    g_simple_async_result_complete_in_idle (G_SIMPLE_ASYNC_RESULT (result));
+    return;
+  }
+  if (data_file[0] != '/') {
+    full_path = g_build_filename (base_path, data_file, NULL);
+  } else {
+    full_path = data_file;
+    data_file = NULL;
+  }
+
+  if (g_stat (full_path, &stat_buf) < 0) {
+    g_simple_async_result_set_error (G_SIMPLE_ASYNC_RESULT (result),
+                                     GRL_NET_WC_ERROR,
+                                     GRL_NET_WC_ERROR_NOT_FOUND,
+                                     "%s",
+                                     "Could not access mock content");
+    g_simple_async_result_complete_in_idle (G_SIMPLE_ASYNC_RESULT (result));
+    if (data_file)
+      g_free (data_file);
+    if (full_path)
+      g_free (full_path);
+    return;
+  }
+  if (data_file)
+    g_free (data_file);
+  if (full_path)
+    g_free (full_path);
+
+  g_simple_async_result_set_op_res_gpointer (G_SIMPLE_ASYNC_RESULT (result),
+                                             new_url,
+                                             NULL);
+  g_simple_async_result_complete_in_idle (G_SIMPLE_ASYNC_RESULT (result));
+}
+
+void
+get_content_mocked (GrlNetWc *self,
+                    void *op,
+                    gchar **content,
+                    gsize *length)
+{
+  char *url = (char *) op;
+  char *data_file = NULL, *full_path = NULL;
+  GError *error = NULL;
+
+  data_file = g_key_file_get_value (config, url, "data", NULL);
+  if (data_file[0] != '/') {
+    full_path = g_build_filename (base_path, data_file, NULL);
+  } else {
+    full_path = data_file;
+    data_file = NULL;
+  }
+  g_file_get_contents (full_path, content, length, &error);
+
+  if (data_file)
+    g_free (data_file);
+
+  if (full_path)
+    g_free (full_path);
+}
+
+void init_mock_requester (GrlNetWc *self)
+{
+  char *config_filename = NULL;
+  base_path = NULL;
+
+  /* Parse environment variable. */
+  {
+    const char *const env = g_getenv (GRL_NET_MOCKED_VAR);
+
+    enable_mocking = env
+            && strcmp(env, "0")
+            && g_ascii_strcasecmp(env, "no")
+            && g_ascii_strcasecmp(env, "off")
+            && g_ascii_strcasecmp(env, "false");
+
+    if (!enable_mocking)
+      return;
+
+    char **tokens = g_strsplit (env, ":", -1);
+
+    for (int i = 0; tokens[i]; ++i) {
+      if (1 == sscanf (tokens[i], "throttle=%u", &throttle_override))
+        continue;
+
+      if (g_str_has_prefix (tokens[i], "config=")) {
+        g_free (config_filename);
+        config_filename = g_strdup (tokens[i] + strlen ("config="));
+        continue;
+      }
+
+      GRL_WARNING ("Unknown token in \"%s\" variable: \"%s\"",
+                   GRL_NET_MOCKED_VAR, tokens[i]);
+    }
+
+    g_strfreev (tokens);
+  }
+
+  /* Read configuration file. */
+  if (config_filename)
+    GRL_DEBUG ("Trying to load mock file \"%s\"", config_filename);
+  else
+    config_filename = g_strdup ("grl-net-mock-data.ini");
+
+  GError *error = NULL;
+  config = g_key_file_new ();
+
+  g_key_file_load_from_file (config, config_filename, G_KEY_FILE_NONE, &error);
+
+  int version = 0;
+
+  if (error) {
+    GRL_WARNING ("Failed to load mock file \"%s\": %s",
+                 config_filename, error->message);
+    g_clear_error (&error);
+  } else {
+    /* Check if we managed to load a file */
+    version = g_key_file_get_integer (config, "default", "version", &error);
+
+    if (error || version < GRL_MOCK_VERSION) {
+      GRL_WARNING ("Unsupported mock version %d.", version);
+      g_clear_error (&error);
+    }
+  }
+
+  if (version < GRL_MOCK_VERSION) {
+    g_key_file_unref (config);
+    config = NULL;
+    return;
+  }
+
+  char **parameter_names = g_key_file_get_string_list (config, "default",
+                                                       "ignored-parameters",
+                                                       NULL, &error);
+  if (error) {
+    parameter_names = NULL;
+    g_clear_error (&error);
+  }
+
+  /* Build regular expressions for ignored query parameters. */
+  if (parameter_names) {
+    GString *pattern = g_string_new ("(?:^|\\&)");
+
+    if (parameter_names[0] && strcmp(parameter_names[0], "*") == 0) {
+      g_string_append (pattern, "[^=&]+");
+    } else {
+      g_string_append (pattern, "(?:");
+
+      for (int i = 0; parameter_names[i]; ++i) {
+        if (i)
+          g_string_append (pattern, "|");
+
+        char *escaped = g_regex_escape_string (parameter_names[i], -1);
+        g_string_append (pattern, escaped);
+        g_free (escaped);
+      }
+
+      g_string_append (pattern, ")(?:=[^&]*)?");
+    }
+
+    ignored_parameters = g_regex_new (pattern->str, G_REGEX_OPTIMIZE, 0, &error);
+
+    if (error) {
+      GRL_WARNING ("Failed to compile regular expression "
+                   "for ignored query parameters: %s", error->message);
+      g_clear_error (&error);
+    }
+  }
+
+  /* Find base path for mock data. */
+  GFile *file = g_file_new_for_commandline_arg (config_filename);
+  GFile *parent = g_file_get_parent (file);
+
+  base_path = g_file_get_path (parent);
+
+  g_object_unref (parent);
+  g_object_unref (file);
+  g_free (config_filename);
+}
+
+void finalize_mock_requester (GrlNetWc *self)
+{
+  if (config) {
+    g_key_file_unref (config);
+  }
+
+  if (base_path) {
+    g_free (base_path);
+  }
+
+  if (ignored_parameters) {
+    g_regex_unref (ignored_parameters);
+  }
+}
+
+void free_mock_op_res (void *op)
+{
+  g_free (op);
+}
diff --git a/libs/net/grl-net-private.c b/libs/net/grl-net-private.c
index 7c1e557..e4287f0 100644
--- a/libs/net/grl-net-private.c
+++ b/libs/net/grl-net-private.c
@@ -29,6 +29,11 @@
 
 #include "grl-net-private.h"
 
+#include <glib/gstdio.h>
+#include <errno.h>
+
+static const char *capture_dir = NULL;
+
 void
 parse_error (guint status,
              const gchar *reason,
@@ -89,3 +94,68 @@ parse_error (guint status,
     g_message ("Unhandled status: %s", soup_status_get_phrase (status));
   }
 }
+
+void
+init_dump_directory ()
+{
+  capture_dir = g_getenv (GRL_NET_CAPTURE_DIR_VAR);
+
+  if (capture_dir && g_mkdir_with_parents (capture_dir, 0700)) {
+    GRL_WARNING ("Could not create capture directory \"%s\": %s",
+                 capture_dir, g_strerror (errno));
+    capture_dir = NULL;
+  }
+}
+
+static char *
+build_request_filename (const char *uri)
+{
+    char *escaped_uri = g_uri_escape_string (uri, NULL, FALSE);
+    char *filename = g_strdup_printf ("%"G_GINT64_FORMAT "-%s",
+                                      g_get_monotonic_time (), escaped_uri);
+
+    g_free (escaped_uri);
+    return filename;
+}
+
+void
+dump_data (SoupURI *uri,
+           const char *buffer,
+           const gsize length)
+{
+  if (!capture_dir)
+    return;
+
+  char *uri_string = soup_uri_to_string (uri, FALSE);
+
+  /* Write request content to file in capture directory. */
+  char *request_filename = build_request_filename (uri_string);
+  char *path = g_build_filename (capture_dir, request_filename, NULL);
+
+  GError *error = NULL;
+  if (!g_file_set_contents (path, buffer, length, &error)) {
+    GRL_WARNING ("Could not write contents to disk: %s", error->message);
+    g_error_free (error);
+  }
+
+  g_free (path);
+
+  /* Append record about the just written file to "grl-net-mock-data-%PID.ini"
+   * in the capture directory. */
+  char *filename = g_strdup_printf ("grl-net-mock-data-%u.ini", getpid());
+  path = g_build_filename (capture_dir, filename, NULL);
+  g_free (filename);
+
+  FILE *stream = g_fopen (path, "at");
+  g_free (path);
+
+  if (!stream) {
+    GRL_WARNING ("Could not write contents to disk: %s", g_strerror (errno));
+  } else {
+    fprintf (stream, "[%s]\ndata=%s\n\n", uri_string, request_filename);
+    fclose (stream);
+  }
+
+  g_free (request_filename);
+  g_free (uri_string);
+}
diff --git a/libs/net/grl-net-private.h b/libs/net/grl-net-private.h
index 3d15a0c..bd5f411 100644
--- a/libs/net/grl-net-private.h
+++ b/libs/net/grl-net-private.h
@@ -33,6 +33,12 @@
 
 G_BEGIN_DECLS
 
+#define GRL_LOG_DOMAIN_DEFAULT wc_log_domain
+GRL_LOG_DOMAIN_EXTERN(wc_log_domain);
+
+#define GRL_NET_CAPTURE_DIR_VAR "GRL_NET_CAPTURE_DIR"
+
+
 struct _GrlNetWcPrivate {
   SoupSession *session;
   SoupLoggerLogLevel log_level;
@@ -76,6 +82,12 @@ guint cache_get_size (GrlNetWc *self);
 
 void free_op_res (void *op);
 
+G_GNUC_INTERNAL
+void init_dump_directory (void);
+
+G_GNUC_INTERNAL
+void dump_data (SoupURI *uri, const gchar *data, gsize length);
+
 G_END_DECLS
 
 #endif /* _GRL_NET_PRIVATE_H_ */
diff --git a/libs/net/grl-net-soup-stable.c b/libs/net/grl-net-soup-stable.c
index 70ae7ec..cdd615d 100644
--- a/libs/net/grl-net-soup-stable.c
+++ b/libs/net/grl-net-soup-stable.c
@@ -147,12 +147,16 @@ get_content (GrlNetWc *self,
 
   if (length)
     *length = (gsize) msg->response_body->length;
+
+  dump_data (soup_message_get_uri (msg),
+             msg->response_body->data,
+             msg->response_body->length);
 }
 
 void
 init_requester (GrlNetWc *self)
 {
-  /* noop */
+  init_dump_directory ();
 }
 
 void
diff --git a/libs/net/grl-net-soup-unstable.c b/libs/net/grl-net-soup-unstable.c
index cc81c38..2178f98 100644
--- a/libs/net/grl-net-soup-unstable.c
+++ b/libs/net/grl-net-soup-unstable.c
@@ -52,6 +52,7 @@ init_requester (GrlNetWc *self)
   priv->requester = soup_requester_new ();
   soup_session_add_feature (priv->session,
                             SOUP_SESSION_FEATURE (priv->requester));
+  init_dump_directory ();
 }
 
 void
@@ -287,6 +288,10 @@ get_content (GrlNetWc *self,
   GrlNetWcPrivate *priv = self->priv;
   struct request_res *rr = op;
 
+  dump_data (soup_request_get_uri (rr->request),
+             rr->buffer,
+             rr->offset);
+
   if (priv->previous_data)
     g_free (priv->previous_data);
 
diff --git a/libs/net/grl-net-wc.c b/libs/net/grl-net-wc.c
index 85efe6d..33dbae6 100644
--- a/libs/net/grl-net-wc.c
+++ b/libs/net/grl-net-wc.c
@@ -44,8 +44,8 @@
 #include <grilo.h>
 #include "grl-net-wc.h"
 #include "grl-net-private.h"
+#include "grl-net-mock-private.h"
 
-#define GRL_LOG_DOMAIN_DEFAULT wc_log_domain
 GRL_LOG_DOMAIN(wc_log_domain);
 
 enum {
@@ -191,6 +191,9 @@ grl_net_wc_init (GrlNetWc *wc)
 
   set_thread_context (wc);
   init_requester (wc);
+  init_mock_requester (wc);
+
+  override_throttling (&wc->priv->throttling);
 }
 
 static void
@@ -203,6 +206,7 @@ grl_net_wc_finalize (GObject *object)
 
   cache_down (wc);
   finalize_requester (wc);
+  finalize_mock_requester (wc);
 
   g_queue_free (wc->priv->pending);
   g_object_unref (wc->priv->session);
@@ -295,7 +299,10 @@ get_url_delayed (gpointer user_data)
     g_assert (c == d);
   }
 
-  get_url_now (c->self, c->url, c->headers, c->result, c->cancellable);
+  if (is_mocked ())
+    get_url_mocked (c->self, c->url, c->headers, c->result, c->cancellable);
+  else
+    get_url_now (c->self, c->url, c->headers, c->result, c->cancellable);
 
   g_free (c->url);
   if (c->headers) {
@@ -321,7 +328,10 @@ get_url (GrlNetWc *self,
   g_get_current_time (&now);
 
   if ((now.tv_sec - priv->last_request.tv_sec) > priv->throttling) {
-    get_url_now (self, url, headers, result, cancellable);
+    if (is_mocked ())
+      get_url_mocked (self, url, headers, result, cancellable);
+    else
+      get_url_now (self, url, headers, result, cancellable);
     g_get_current_time (&priv->last_request);
 
     return;
@@ -477,6 +487,7 @@ grl_net_wc_request_with_headers_hash_async (GrlNetWc *self,
   get_url (self, uri, headers, G_ASYNC_RESULT (result), cancellable);
 }
 
+
 /**
  * grl_net_wc_request_finish:
  * @self: a #GrlNetWc instance
@@ -515,10 +526,16 @@ grl_net_wc_request_finish (GrlNetWc *self,
     goto end_func;
   }
 
-  get_content(self, op, content, length);
+  if (is_mocked ())
+    get_content_mocked (self, op, content, length);
+  else
+    get_content(self, op, content, length);
 
 end_func:
-  free_op_res (op);
+  if (is_mocked ())
+    free_mock_op_res (op);
+  else
+    free_op_res (op);
 
   return ret;
 }
@@ -566,6 +583,9 @@ grl_net_wc_set_throttling (GrlNetWc *self,
 {
   g_return_if_fail (GRL_IS_NET_WC (self));
 
+  if (override_throttling (&throttling))
+    GRL_WARNING ("Overriding throttle setting for mocking");
+
   if (throttling > 0) {
     /* max conns per host = 1 */
     g_object_set (self->priv->session,
