diff --git a/Cargo.lock b/Cargo.lock
index 5f7c52e0a17..f695a7f2cb8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1113,6 +1113,7 @@ dependencies = [
  "ra_proc_macro",
  "ra_tt",
  "serde_derive",
+ "test_utils",
 ]
 
 [[package]]
diff --git a/crates/ra_proc_macro_srv/Cargo.toml b/crates/ra_proc_macro_srv/Cargo.toml
index 437b8f475f1..1e0f5033920 100644
--- a/crates/ra_proc_macro_srv/Cargo.toml
+++ b/crates/ra_proc_macro_srv/Cargo.toml
@@ -14,6 +14,7 @@ ra_mbe = { path = "../ra_mbe" }
 ra_proc_macro = { path = "../ra_proc_macro" }
 goblin = "0.2.1"
 libloading = "0.6.0"
+test_utils = { path = "../test_utils" }
 
 [dev-dependencies]
 cargo_metadata = "0.9.1"
diff --git a/crates/ra_proc_macro_srv/src/lib.rs b/crates/ra_proc_macro_srv/src/lib.rs
index 8fba73ac97a..59716cbb3b0 100644
--- a/crates/ra_proc_macro_srv/src/lib.rs
+++ b/crates/ra_proc_macro_srv/src/lib.rs
@@ -52,3 +52,6 @@ pub fn list_macros(task: &ListMacrosTask) -> Result<ListMacrosResult, String> {
         }
     }
 }
+
+#[cfg(test)]
+mod tests;
diff --git a/crates/ra_proc_macro_srv/src/tests/fixtures/test_serialize_proc_macro.txt b/crates/ra_proc_macro_srv/src/tests/fixtures/test_serialize_proc_macro.txt
new file mode 100644
index 00000000000..24507d98d76
--- /dev/null
+++ b/crates/ra_proc_macro_srv/src/tests/fixtures/test_serialize_proc_macro.txt
@@ -0,0 +1,188 @@
+SUBTREE $
+  PUNCH   # [alone] 4294967295
+  SUBTREE [] 4294967295
+    IDENT   allow 4294967295
+    SUBTREE () 4294967295
+      IDENT   non_upper_case_globals 4294967295
+      PUNCH   , [alone] 4294967295
+      IDENT   unused_attributes 4294967295
+      PUNCH   , [alone] 4294967295
+      IDENT   unused_qualifications 4294967295
+  IDENT   const 4294967295
+  IDENT   _IMPL_SERIALIZE_FOR_Foo 4294967295
+  PUNCH   : [alone] 4294967295
+  SUBTREE () 4294967295
+  PUNCH   = [alone] 4294967295
+  SUBTREE {} 4294967295
+    PUNCH   # [alone] 4294967295
+    SUBTREE [] 4294967295
+      IDENT   allow 4294967295
+      SUBTREE () 4294967295
+        IDENT   unknown_lints 4294967295
+    PUNCH   # [alone] 4294967295
+    SUBTREE [] 4294967295
+      IDENT   cfg_attr 4294967295
+      SUBTREE () 4294967295
+        IDENT   feature 4294967295
+        PUNCH   = [alone] 4294967295
+        SUBTREE $
+          LITERAL "cargo-clippy" 0
+        PUNCH   , [alone] 4294967295
+        IDENT   allow 4294967295
+        SUBTREE () 4294967295
+          IDENT   useless_attribute 4294967295
+    PUNCH   # [alone] 4294967295
+    SUBTREE [] 4294967295
+      IDENT   allow 4294967295
+      SUBTREE () 4294967295
+        IDENT   rust_2018_idioms 4294967295
+    IDENT   extern 4294967295
+    IDENT   crate 4294967295
+    IDENT   serde 4294967295
+    IDENT   as 4294967295
+    IDENT   _serde 4294967295
+    PUNCH   ; [alone] 4294967295
+    PUNCH   # [alone] 4294967295
+    SUBTREE [] 4294967295
+      IDENT   allow 4294967295
+      SUBTREE () 4294967295
+        IDENT   unused_macros 4294967295
+    IDENT   macro_rules 4294967295
+    PUNCH   ! [alone] 4294967295
+    IDENT   try 4294967295
+    SUBTREE {} 4294967295
+      SUBTREE () 4294967295
+        PUNCH   $ [alone] 4294967295
+        IDENT   __expr 4294967295
+        PUNCH   : [alone] 4294967295
+        IDENT   expr 4294967295
+      PUNCH   = [joint] 4294967295
+      PUNCH   > [alone] 4294967295
+      SUBTREE {} 4294967295
+        IDENT   match 4294967295
+        PUNCH   $ [alone] 4294967295
+        IDENT   __expr 4294967295
+        SUBTREE {} 4294967295
+          IDENT   _serde 4294967295
+          PUNCH   : [joint] 4294967295
+          PUNCH   : [alone] 4294967295
+          IDENT   export 4294967295
+          PUNCH   : [joint] 4294967295
+          PUNCH   : [alone] 4294967295
+          IDENT   Ok 4294967295
+          SUBTREE () 4294967295
+            IDENT   __val 4294967295
+          PUNCH   = [joint] 4294967295
+          PUNCH   > [alone] 4294967295
+          IDENT   __val 4294967295
+          PUNCH   , [alone] 4294967295
+          IDENT   _serde 4294967295
+          PUNCH   : [joint] 4294967295
+          PUNCH   : [alone] 4294967295
+          IDENT   export 4294967295
+          PUNCH   : [joint] 4294967295
+          PUNCH   : [alone] 4294967295
+          IDENT   Err 4294967295
+          SUBTREE () 4294967295
+            IDENT   __err 4294967295
+          PUNCH   = [joint] 4294967295
+          PUNCH   > [alone] 4294967295
+          SUBTREE {} 4294967295
+            IDENT   return 4294967295
+            IDENT   _serde 4294967295
+            PUNCH   : [joint] 4294967295
+            PUNCH   : [alone] 4294967295
+            IDENT   export 4294967295
+            PUNCH   : [joint] 4294967295
+            PUNCH   : [alone] 4294967295
+            IDENT   Err 4294967295
+            SUBTREE () 4294967295
+              IDENT   __err 4294967295
+            PUNCH   ; [alone] 4294967295
+    PUNCH   # [alone] 4294967295
+    SUBTREE [] 4294967295
+      IDENT   automatically_derived 4294967295
+    IDENT   impl 4294967295
+    IDENT   _serde 4294967295
+    PUNCH   : [joint] 4294967295
+    PUNCH   : [alone] 4294967295
+    IDENT   Serialize 4294967295
+    IDENT   for 4294967295
+    IDENT   Foo 1
+    SUBTREE {} 4294967295
+      IDENT   fn 4294967295
+      IDENT   serialize 4294967295
+      PUNCH   < [alone] 4294967295
+      IDENT   __S 4294967295
+      PUNCH   > [alone] 4294967295
+      SUBTREE () 4294967295
+        PUNCH   & [alone] 4294967295
+        IDENT   self 4294967295
+        PUNCH   , [alone] 4294967295
+        IDENT   __serializer 4294967295
+        PUNCH   : [alone] 4294967295
+        IDENT   __S 4294967295
+      PUNCH   - [joint] 4294967295
+      PUNCH   > [alone] 4294967295
+      IDENT   _serde 4294967295
+      PUNCH   : [joint] 4294967295
+      PUNCH   : [alone] 4294967295
+      IDENT   export 4294967295
+      PUNCH   : [joint] 4294967295
+      PUNCH   : [alone] 4294967295
+      IDENT   Result 4294967295
+      PUNCH   < [alone] 4294967295
+      IDENT   __S 4294967295
+      PUNCH   : [joint] 4294967295
+      PUNCH   : [alone] 4294967295
+      IDENT   Ok 4294967295
+      PUNCH   , [alone] 4294967295
+      IDENT   __S 4294967295
+      PUNCH   : [joint] 4294967295
+      PUNCH   : [alone] 4294967295
+      IDENT   Error 4294967295
+      PUNCH   > [alone] 4294967295
+      IDENT   where 4294967295
+      IDENT   __S 4294967295
+      PUNCH   : [alone] 4294967295
+      IDENT   _serde 4294967295
+      PUNCH   : [joint] 4294967295
+      PUNCH   : [alone] 4294967295
+      IDENT   Serializer 4294967295
+      PUNCH   , [alone] 4294967295
+      SUBTREE {} 4294967295
+        IDENT   let 4294967295
+        IDENT   __serde_state 4294967295
+        PUNCH   = [alone] 4294967295
+        IDENT   try 4294967295
+        PUNCH   ! [alone] 4294967295
+        SUBTREE () 4294967295
+          IDENT   _serde 4294967295
+          PUNCH   : [joint] 4294967295
+          PUNCH   : [alone] 4294967295
+          IDENT   Serializer 4294967295
+          PUNCH   : [joint] 4294967295
+          PUNCH   : [alone] 4294967295
+          IDENT   serialize_struct 4294967295
+          SUBTREE () 4294967295
+            IDENT   __serializer 4294967295
+            PUNCH   , [alone] 4294967295
+            LITERAL "Foo" 4294967295
+            PUNCH   , [alone] 4294967295
+            IDENT   false 4294967295
+            IDENT   as 4294967295
+            IDENT   usize 4294967295
+        PUNCH   ; [alone] 4294967295
+        IDENT   _serde 4294967295
+        PUNCH   : [joint] 4294967295
+        PUNCH   : [alone] 4294967295
+        IDENT   ser 4294967295
+        PUNCH   : [joint] 4294967295
+        PUNCH   : [alone] 4294967295
+        IDENT   SerializeStruct 4294967295
+        PUNCH   : [joint] 4294967295
+        PUNCH   : [alone] 4294967295
+        IDENT   end 4294967295
+        SUBTREE () 4294967295
+          IDENT   __serde_state 4294967295
+  PUNCH   ; [alone] 4294967295
\ No newline at end of file
diff --git a/crates/ra_proc_macro_srv/src/tests/mod.rs b/crates/ra_proc_macro_srv/src/tests/mod.rs
new file mode 100644
index 00000000000..03f79bc5d60
--- /dev/null
+++ b/crates/ra_proc_macro_srv/src/tests/mod.rs
@@ -0,0 +1,47 @@
+//! proc-macro tests
+
+#[macro_use]
+mod utils;
+use test_utils::assert_eq_text;
+use utils::*;
+
+#[test]
+fn test_derive_serialize_proc_macro() {
+    assert_expand(
+        "serde_derive",
+        "Serialize",
+        "1.0.104",
+        r##"struct Foo {}"##,
+        include_str!("fixtures/test_serialize_proc_macro.txt"),
+    );
+}
+
+#[test]
+fn test_derive_serialize_proc_macro_failed() {
+    assert_expand(
+        "serde_derive",
+        "Serialize",
+        "1.0.104",
+        r##"
+    struct {}
+"##,
+        r##"
+SUBTREE $
+  IDENT   compile_error 4294967295
+  PUNCH   ! [alone] 4294967295
+  SUBTREE {} 4294967295
+    LITERAL "expected identifier" 4294967295
+"##,
+    );
+}
+
+#[test]
+fn test_derive_proc_macro_list() {
+    let res = list("serde_derive", "1.0.104").join("\n");
+
+    assert_eq_text!(
+        &res,
+        r#"Serialize [CustomDerive]
+Deserialize [CustomDerive]"#
+    );
+}
diff --git a/crates/ra_proc_macro_srv/src/tests/utils.rs b/crates/ra_proc_macro_srv/src/tests/utils.rs
new file mode 100644
index 00000000000..1ee40944922
--- /dev/null
+++ b/crates/ra_proc_macro_srv/src/tests/utils.rs
@@ -0,0 +1,65 @@
+//! utils used in proc-macro tests
+
+use crate::dylib;
+use crate::list_macros;
+pub use difference::Changeset as __Changeset;
+use ra_proc_macro::ListMacrosTask;
+use std::str::FromStr;
+use test_utils::assert_eq_text;
+
+mod fixtures {
+    use cargo_metadata::{parse_messages, Message};
+    use std::process::Command;
+
+    // Use current project metadata to get the proc-macro dylib path
+    pub fn dylib_path(crate_name: &str, version: &str) -> std::path::PathBuf {
+        let command = Command::new("cargo")
+            .args(&["check", "--message-format", "json"])
+            .output()
+            .unwrap()
+            .stdout;
+
+        for message in parse_messages(command.as_slice()) {
+            match message.unwrap() {
+                Message::CompilerArtifact(artifact) => {
+                    if artifact.target.kind.contains(&"proc-macro".to_string()) {
+                        let repr = format!("{} {}", crate_name, version);
+                        if artifact.package_id.repr.starts_with(&repr) {
+                            return artifact.filenames[0].clone();
+                        }
+                    }
+                }
+                _ => (), // Unknown message
+            }
+        }
+
+        panic!("No proc-macro dylib for {} found!", crate_name);
+    }
+}
+
+fn parse_string(code: &str) -> Option<crate::rustc_server::TokenStream> {
+    Some(crate::rustc_server::TokenStream::from_str(code).unwrap())
+}
+
+pub fn assert_expand(
+    crate_name: &str,
+    macro_name: &str,
+    version: &str,
+    fixture: &str,
+    expect: &str,
+) {
+    let path = fixtures::dylib_path(crate_name, version);
+    let expander = dylib::Expander::new(&path).unwrap();
+    let fixture = parse_string(fixture).unwrap();
+
+    let res = expander.expand(macro_name, &fixture.subtree, None).unwrap();
+    assert_eq_text!(&format!("{:?}", res), &expect.trim());
+}
+
+pub fn list(crate_name: &str, version: &str) -> Vec<String> {
+    let path = fixtures::dylib_path(crate_name, version);
+    let task = ListMacrosTask { lib: path };
+
+    let res = list_macros(&task).unwrap();
+    res.macros.into_iter().map(|(name, kind)| format!("{} [{:?}]", name, kind)).collect()
+}