Repository: martinohmann/hcl-rs
Pull request #152 (2023-01-06 21:56:53)

chore(test): convert most of the tests into integration tests
Also improves some test cases here and there and removes redundant ones. There's still a lot to do to improve the tests in general.


Patch:
diff --git a/Cargo.toml b/Cargo.toml
index be0b178..a50caf1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -49,6 +49,7 @@ version = "0.1.9"
 [dev-dependencies]
 criterion = "0.4"
 assert-json-diff = "2.0.2"
+indoc = "1.0"
 pretty_assertions = "1.3.0"
 textwrap = "0.16.0"
 
diff --git a/src/de/mod.rs b/src/de/mod.rs
index 40165b4..936b84e 100644
--- a/src/de/mod.rs
+++ b/src/de/mod.rs
@@ -5,9 +5,6 @@
 //!
 //! [hcl-json-spec]: https://github.com/hashicorp/hcl/blob/main/json/spec.md
 
-#[cfg(test)]
-mod tests;
-
 use crate::structure::IntoJsonSpec;
 use crate::{parser, Body, Error, Identifier, Result};
 use serde::de::value::StringDeserializer;
diff --git a/src/eval/func.rs b/src/eval/func.rs
index 79f42db..f2847f3 100644
--- a/src/eval/func.rs
+++ b/src/eval/func.rs
@@ -437,3 +437,45 @@ impl<'a> Iterator for VariadicArgs<'a> {
         self.iter.next()
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn param_type() {
+        let string = Value::from("a string");
+        let number = Value::from(42);
+        let boolean = Value::from(true);
+        let string_array = Value::from_iter(["foo", "bar"]);
+        let number_array = Value::from_iter([1, 2, 3]);
+        let object_of_strings = Value::from_iter([("foo", "bar"), ("baz", "qux")]);
+        let object_of_numbers = Value::from_iter([("foo", 1), ("bar", 2)]);
+
+        let param = ParamType::String;
+        assert!(param.is_satisfied_by(&string));
+        assert!(!param.is_satisfied_by(&number));
+
+        let param = ParamType::Any;
+        assert!(param.is_satisfied_by(&string));
+        assert!(param.is_satisfied_by(&number));
+
+        let param = ParamType::nullable(ParamType::String);
+        assert!(param.is_satisfied_by(&string));
+        assert!(param.is_satisfied_by(&Value::Null));
+        assert!(!param.is_satisfied_by(&number));
+
+        let param = ParamType::one_of([ParamType::String, ParamType::Number]);
+        assert!(param.is_satisfied_by(&string));
+        assert!(param.is_satisfied_by(&number));
+        assert!(!param.is_satisfied_by(&boolean));
+
+        let param = ParamType::array_of(ParamType::String);
+        assert!(param.is_satisfied_by(&string_array));
+        assert!(!param.is_satisfied_by(&number_array));
+
+        let param = ParamType::object_of(ParamType::String);
+        assert!(param.is_satisfied_by(&object_of_strings));
+        assert!(!param.is_satisfied_by(&object_of_numbers));
+    }
+}
diff --git a/src/eval/mod.rs b/src/eval/mod.rs
index b7cb3c9..3ad4953 100644
--- a/src/eval/mod.rs
+++ b/src/eval/mod.rs
@@ -205,8 +205,6 @@ mod expr;
 mod func;
 mod impls;
 mod template;
-#[cfg(test)]
-mod tests;
 
 pub use self::error::{Error, ErrorKind, EvalResult};
 pub use self::func::{
diff --git a/src/expr/mod.rs b/src/expr/mod.rs
index 349cbbe..173a12f 100644
--- a/src/expr/mod.rs
+++ b/src/expr/mod.rs
@@ -10,8 +10,6 @@ mod func_call;
 mod operation;
 pub(crate) mod ser;
 mod template_expr;
-#[cfg(test)]
-mod tests;
 mod traversal;
 mod variable;
 
diff --git a/src/expr/operation.rs b/src/expr/operation.rs
index 977e361..bc85b04 100644
--- a/src/expr/operation.rs
+++ b/src/expr/operation.rs
@@ -300,3 +300,72 @@ impl FromStr for BinaryOperator {
         }
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use pretty_assertions::assert_eq;
+
+    macro_rules! binop {
+        ($l:expr, $op:expr, $r:expr $(,)?) => {
+            BinaryOp::new($l, $op, $r)
+        };
+    }
+
+    macro_rules! assert_normalizes_to {
+        ($op:expr, $expected:expr $(,)?) => {
+            assert_eq!($op.normalize(), $expected);
+        };
+    }
+
+    #[test]
+    fn normalize_binary_op() {
+        use BinaryOperator::{Div, Mod, Mul, Plus};
+
+        assert_normalizes_to!(
+            binop!(binop!(1, Plus, 2), Div, binop!(3, Mul, 4)),
+            binop!(1, Plus, binop!(2, Div, binop!(3, Mul, 4))),
+        );
+
+        assert_normalizes_to!(
+            binop!(binop!(1, Div, 2), Mul, binop!(3, Plus, binop!(4, Mod, 5))),
+            binop!(binop!(binop!(1, Div, 2), Mul, 3), Plus, binop!(4, Mod, 5)),
+        );
+
+        assert_normalizes_to!(
+            binop!(binop!(binop!(1, Plus, 2), Mul, 3), Div, 4),
+            binop!(1, Plus, binop!(binop!(2, Mul, 3), Div, 4)),
+        );
+
+        assert_normalizes_to!(
+            binop!(1, Div, binop!(binop!(2, Plus, 3), Mul, 4)),
+            binop!(binop!(1, Div, 2), Plus, binop!(3, Mul, 4)),
+        );
+    }
+
+    #[test]
+    fn normalize_parenthesized() {
+        use BinaryOperator::{Div, Mod, Mul, Plus};
+
+        fn parens(op: BinaryOp) -> Expression {
+            Expression::Parenthesis(Box::new(op.into()))
+        }
+
+        let op = binop!(
+            parens(binop!(1, Div, 2)),
+            Mul,
+            parens(binop!(3, Mod, parens(binop!(4, Plus, 5)))),
+        );
+
+        assert_normalizes_to!(op.clone(), op);
+    }
+
+    #[test]
+    fn already_normalized() {
+        use BinaryOperator::Plus;
+
+        let op = binop!(binop!(1, Plus, 2), Plus, binop!(3, Plus, 4));
+
+        assert_normalizes_to!(op.clone(), op);
+    }
+}
diff --git a/src/format/mod.rs b/src/format/mod.rs
index 3b02497..ef80bef 100644
--- a/src/format/mod.rs
+++ b/src/format/mod.rs
@@ -36,8 +36,6 @@
 
 mod escape;
 mod impls;
-#[cfg(test)]
-mod tests;
 
 use self::escape::{CharEscape, ESCAPE};
 use crate::Result;
@@ -766,3 +764,19 @@ where
     string.push('}');
     Ok(string)
 }
+
+#[cfg(test)]
+mod tests {
+    use super::to_interpolated_string;
+    use crate::expr::{BinaryOp, BinaryOperator, FuncCall};
+    use pretty_assertions::assert_eq;
+
+    #[test]
+    fn format_interpolated_string() {
+        let binop = BinaryOp::new(1, BinaryOperator::Plus, 1);
+        assert_eq!(to_interpolated_string(&binop).unwrap(), "${1 + 1}");
+
+        let expr = FuncCall::builder("add").arg(1).arg(1).build();
+        assert_eq!(to_interpolated_string(&expr).unwrap(), "${add(1, 1)}");
+    }
+}
diff --git a/src/ser/mod.rs b/src/ser/mod.rs
index 1c50a03..23b66a3 100644
--- a/src/ser/mod.rs
+++ b/src/ser/mod.rs
@@ -264,8 +264,6 @@
 //! ```
 
 pub(crate) mod blocks;
-#[cfg(test)]
-mod tests;
 
 pub use self::blocks::{block, doubly_labeled_block, labeled_block, Block, LabeledBlock};
 use crate::format::{Format, Formatter};


Test patch:
diff --git a/src/expr/tests.rs b/src/expr/tests.rs
deleted file mode 100644
index a83b873..0000000
--- a/src/expr/tests.rs
+++ /dev/null
@@ -1,91 +0,0 @@
-use super::{BinaryOp, BinaryOperator, Expression, Operation};
-use pretty_assertions::assert_eq;
-
-#[test]
-fn normalize_binary_op() {
-    use {BinaryOperator::*, Operation::*};
-
-    let op = BinaryOp::new(
-        Binary(BinaryOp::new(1, Plus, 2)),
-        Div,
-        Binary(BinaryOp::new(3, Mul, 4)),
-    );
-
-    let expected = BinaryOp::new(
-        1,
-        Plus,
-        Binary(BinaryOp::new(2, Div, Binary(BinaryOp::new(3, Mul, 4)))),
-    );
-
-    assert_eq!(op.normalize(), expected);
-
-    let op = BinaryOp::new(
-        Binary(BinaryOp::new(1, Plus, 2)),
-        Plus,
-        Binary(BinaryOp::new(3, Plus, 4)),
-    );
-
-    let expected = op.clone();
-
-    assert_eq!(op.normalize(), expected);
-
-    let op = BinaryOp::new(
-        Binary(BinaryOp::new(1, Div, 2)),
-        Mul,
-        Binary(BinaryOp::new(3, Plus, Binary(BinaryOp::new(4, Mod, 5)))),
-    );
-
-    let expected = BinaryOp::new(
-        Binary(BinaryOp::new(Binary(BinaryOp::new(1, Div, 2)), Mul, 3)),
-        Plus,
-        Binary(BinaryOp::new(4, Mod, 5)),
-    );
-
-    assert_eq!(op.normalize(), expected);
-
-    fn sub_expr(op: Operation) -> Expression {
-        Expression::Parenthesis(Box::new(Expression::from(op)))
-    }
-
-    let op = BinaryOp::new(
-        sub_expr(Binary(BinaryOp::new(1, Div, 2))),
-        Mul,
-        sub_expr(Binary(BinaryOp::new(
-            3,
-            Mod,
-            sub_expr(Binary(BinaryOp::new(4, Plus, 5))),
-        ))),
-    );
-
-    let expected = op.clone();
-
-    assert_eq!(op.normalize(), expected);
-
-    let op = BinaryOp::new(
-        Binary(BinaryOp::new(Binary(BinaryOp::new(1, Plus, 2)), Mul, 3)),
-        Div,
-        4,
-    );
-
-    let expected = BinaryOp::new(
-        1,
-        Plus,
-        Binary(BinaryOp::new(Binary(BinaryOp::new(2, Mul, 3)), Div, 4)),
-    );
-
-    assert_eq!(op.normalize(), expected);
-
-    let op = BinaryOp::new(
-        1,
-        Div,
-        Binary(BinaryOp::new(Binary(BinaryOp::new(2, Plus, 3)), Mul, 4)),
-    );
-
-    let expected = BinaryOp::new(
-        Binary(BinaryOp::new(1, Div, 2)),
-        Plus,
-        Binary(BinaryOp::new(3, Mul, 4)),
-    );
-
-    assert_eq!(op.normalize(), expected);
-}
diff --git a/src/format/tests.rs b/src/format/tests.rs
deleted file mode 100644
index fef97d7..0000000
--- a/src/format/tests.rs
+++ /dev/null
@@ -1,167 +0,0 @@
-use super::*;
-use crate::expr::{
-    BinaryOp, BinaryOperator, Expression, FuncCall, Operation, TemplateExpr, Variable,
-};
-use crate::structure::Attribute;
-use crate::template::{ForDirective, IfDirective, StripMode, Template};
-use crate::Identifier;
-
-#[track_caller]
-fn expect_format<T: Format>(value: T, expected: &str) {
-    assert_eq!(to_string(&value).unwrap(), expected);
-}
-
-fn expect_formatb<'a, F, T>(f: F, value: T, expected: &str)
-where
-    T: Format,
-    F: FnOnce(FormatterBuilder<'a>) -> FormatterBuilder<'a>,
-{
-    let mut fmt = f(Formatter::builder()).build_vec();
-    let formatted = value.format_string(&mut fmt).unwrap();
-    assert_eq!(formatted, expected);
-}
-
-#[test]
-fn issue_87() {
-    let expr = Expression::from(
-        FuncCall::builder("foo")
-            .arg(Expression::from_iter([("bar", FuncCall::new("baz"))]))
-            .build(),
-    );
-    expect_format(expr, "foo({ \"bar\" = baz() })");
-}
-
-#[test]
-fn issue_91() {
-    expect_format(Attribute::new("_foo", "bar"), "_foo = \"bar\"\n");
-}
-
-#[test]
-fn compact_func_args() {
-    expect_format(
-        FuncCall::builder("func")
-            .arg(vec![1, 2, 3])
-            .arg(expression!({
-                foo = "bar"
-                baz = "qux"
-            }))
-            .build(),
-        "func([1, 2, 3], { foo = \"bar\", baz = \"qux\" })",
-    );
-}
-
-#[test]
-fn compact_arrays() {
-    let attr = Attribute::new("array", expression!([1, 2, 3, [4, 5]]));
-
-    expect_formatb(
-        |b| b.compact_arrays(true),
-        attr,
-        "array = [1, 2, 3, [4, 5]]\n",
-    );
-}
-
-#[test]
-fn compact_objects() {
-    let attr = Attribute::new(
-        "object",
-        expression!({
-            foo = {
-                bar = "baz"
-            }
-            qux = "bam"
-        }),
-    );
-
-    expect_formatb(
-        |b| b.compact_objects(true),
-        attr,
-        "object = { foo = { bar = \"baz\" }, qux = \"bam\" }\n",
-    );
-}
-
-#[test]
-fn template() {
-    let template = Template::new().add_directive(
-        ForDirective::new(
-            Identifier::unchecked("item"),
-            Variable::unchecked("items"),
-            Template::new()
-                .add_literal("\nHello ")
-                .add_directive(
-                    IfDirective::new(
-                        Operation::from(BinaryOp::new(
-                            Variable::unchecked("item"),
-                            BinaryOperator::Eq,
-                            "world",
-                        )),
-                        Template::new().add_literal(" World! "),
-                    )
-                    .with_false_template(
-                        Template::new()
-                            .add_literal(" ")
-                            .add_interpolation(Variable::unchecked("item"))
-                            .add_literal("."),
-                    )
-                    .with_if_strip(StripMode::Start)
-                    .with_else_strip(StripMode::Both)
-                    .with_endif_strip(StripMode::End),
-                )
-                .add_literal("\n"),
-        )
-        .with_for_strip(StripMode::End)
-        .with_endfor_strip(StripMode::End),
-    );
-
-    let expected = r#"%{ for item in items ~}
-Hello %{~ if item == "world" } World! %{~ else ~} ${item}.%{ endif ~}
-%{ endfor ~}"#;
-
-    expect_format(template, expected);
-}
-
-#[test]
-fn issue_131() {
-    expect_format(
-        Attribute::new("a", TemplateExpr::from("${\"b\"}")),
-        "a = \"${\"b\"}\"\n",
-    );
-
-    expect_format(value!({ a = "${\"b\"}" }), "{\n  \"a\" = \"${\"b\"}\"\n}");
-}
-
-#[test]
-fn prefer_ident_keys() {
-    let attr = Attribute::new(
-        "object",
-        expression!({
-            "foo" = 1
-            bar = 2
-            "baz qux" = 3
-        }),
-    );
-
-    expect_formatb(
-        |b| b.prefer_ident_keys(false),
-        &attr,
-        "object = {\n  \"foo\" = 1\n  bar = 2\n  \"baz qux\" = 3\n}\n",
-    );
-
-    expect_formatb(
-        |b| b.prefer_ident_keys(true),
-        &attr,
-        "object = {\n  foo = 1\n  bar = 2\n  \"baz qux\" = 3\n}\n",
-    );
-}
-
-#[test]
-fn to_interpolated_string() {
-    let binop = BinaryOp::new(1, BinaryOperator::Plus, 1);
-    assert_eq!(super::to_interpolated_string(&binop).unwrap(), "${1 + 1}");
-
-    let expr = Expression::from(FuncCall::builder("add").arg(1).arg(1).build());
-    assert_eq!(
-        super::to_interpolated_string(&expr).unwrap(),
-        "${add(1, 1)}"
-    );
-}
diff --git a/src/parser/tests.rs b/src/parser/tests.rs
index 683a600..c269d93 100644
--- a/src/parser/tests.rs
+++ b/src/parser/tests.rs
@@ -1,17 +1,5 @@
 use super::*;
-use crate::expr::{
-    BinaryOp, BinaryOperator, Expression, Heredoc, HeredocStripMode, ObjectKey, Operation,
-    TemplateExpr, Traversal, TraversalOperator, Variable,
-};
-use crate::structure::{Attribute, Block, Body};
-use crate::template::{IfDirective, StripMode, Template};
 use pest::*;
-use pretty_assertions::assert_eq;
-
-#[track_caller]
-fn expect_body(input: &str, expected: Body) {
-    assert_eq!(parse(input).unwrap(), expected);
-}
 
 #[test]
 fn parse_identifier() {
@@ -321,29 +309,6 @@ fn parse_cond_in_interpolation() {
     };
 }
 
-#[test]
-fn parse_object_with_variable_expr_key() {
-    let input = "providers = { aws.eu-central-1 = aws.eu-central-1 }";
-
-    let expected = Body::builder()
-        .add_attribute((
-            "providers",
-            Expression::from_iter([(
-                ObjectKey::Expression(Expression::from(Traversal::new(
-                    Variable::unchecked("aws"),
-                    [Identifier::unchecked("eu-central-1")],
-                ))),
-                Expression::from(Traversal::new(
-                    Variable::unchecked("aws"),
-                    [Identifier::unchecked("eu-central-1")],
-                )),
-            )]),
-        ))
-        .build();
-
-    expect_body(input, expected);
-}
-
 #[test]
 fn parse_nested_function_call_with_splat() {
     parses_to! {
@@ -398,49 +363,6 @@ fn parse_nested_function_call_with_splat() {
     };
 }
 
-#[test]
-fn parse_traversal_with_expression() {
-    let input =
-        "route_table_id = aws_route_table.private[count.index % var.availability_zone_count].id";
-
-    let expected = Body::builder()
-        .add_attribute((
-            "route_table_id",
-            Expression::from(Traversal::new(
-                Variable::unchecked("aws_route_table"),
-                [
-                    TraversalOperator::GetAttr("private".into()),
-                    TraversalOperator::Index(Expression::from(Operation::Binary(BinaryOp::new(
-                        Traversal::new(
-                            Variable::unchecked("count"),
-                            [TraversalOperator::GetAttr("index".into())],
-                        ),
-                        BinaryOperator::Mod,
-                        Traversal::new(
-                            Variable::unchecked("var"),
-                            [TraversalOperator::GetAttr("availability_zone_count".into())],
-                        ),
-                    )))),
-                    TraversalOperator::GetAttr("id".into()),
-                ],
-            )),
-        ))
-        .build();
-
-    expect_body(input, expected);
-}
-
-#[test]
-fn parse_null_in_variable_expr() {
-    let input = "foo = null_foo";
-
-    let expected = Body::builder()
-        .add_attribute(("foo", Variable::unchecked("null_foo")))
-        .build();
-
-    expect_body(input, expected);
-}
-
 #[test]
 fn parse_escaped_slash_in_string() {
     parses_to! {
@@ -454,152 +376,3 @@ fn parse_escaped_slash_in_string() {
         ]
     };
 }
-
-#[test]
-fn parse_hcl() {
-    let input = r#"
-        resource "aws_s3_bucket" "mybucket" {
-          bucket        = "mybucket"
-          force_destroy = true
-
-          server_side_encryption_configuration {
-            rule {
-              apply_server_side_encryption_by_default {
-                kms_master_key_id = aws_kms_key.mykey.arn
-                sse_algorithm     = "aws:kms"
-              }
-            }
-          }
-        }"#;
-
-    let expected = Body::builder()
-        .add_block(
-            Block::builder("resource")
-                .add_label("aws_s3_bucket")
-                .add_label("mybucket")
-                .add_attribute(Attribute::new("bucket", "mybucket"))
-                .add_attribute(Attribute::new("force_destroy", true))
-                .add_block(
-                    Block::builder("server_side_encryption_configuration")
-                        .add_block(
-                            Block::builder("rule")
-                                .add_block(
-                                    Block::builder("apply_server_side_encryption_by_default")
-                                        .add_attribute(Attribute::new(
-                                            "kms_master_key_id",
-                                            Traversal::new(
-                                                Variable::unchecked("aws_kms_key"),
-                                                ["mykey", "arn"],
-                                            ),
-                                        ))
-                                        .add_attribute(Attribute::new("sse_algorithm", "aws:kms"))
-                                        .build(),
-                                )
-                                .build(),
-                        )
-                        .build(),
-                )
-                .build(),
-        )
-        .build();
-
-    expect_body(input, expected);
-}
-
-#[test]
-fn unescape_strings() {
-    let input = r#"
-        block "label\\with\\backslashes" {
-          string_attr = "I \u2665 unicode"
-
-          object_attr = {
-            "key\nwith\nnewlines" = true
-          }
-
-          heredoc = <<-EOS
-            heredoc template with \
-            escaped newline and \\backslash is not unescaped yet
-          EOS
-        }
-    "#;
-
-    let expected = Body::builder()
-        .add_block(
-            Block::builder("block")
-                .add_label("label\\with\\backslashes")
-                .add_attribute(("string_attr", "I \u{2665} unicode"))
-                .add_attribute((
-                    "object_attr",
-                    Expression::from_iter([("key\nwith\nnewlines", true)]),
-                ))
-                .add_attribute((
-                    "heredoc",
-                    TemplateExpr::Heredoc(
-                        Heredoc::new(
-                            Identifier::unchecked("EOS"),
-                            "            heredoc template with \\\n            escaped newline and \\\\backslash is not unescaped yet\n"
-                        )
-                        .with_strip_mode(HeredocStripMode::Indent)
-                    )
-                ))
-                .build(),
-        )
-        .build();
-
-    expect_body(input, expected);
-}
-
-#[test]
-fn parse_negative_numbers() {
-    let input = r#"
-        float = -4.2
-        float_exp = -4.2e10
-        signed = -42
-    "#;
-
-    let expected = Body::builder()
-        .add_attribute(("float", -4.2f64))
-        .add_attribute(("float_exp", -4.2e10f64))
-        .add_attribute(("signed", -42))
-        .build();
-
-    expect_body(input, expected);
-}
-
-#[test]
-fn parse_template_expr() {
-    let input = r#"foo = "bar ${baz} %{~ if cond}qux%{ endif ~}""#;
-
-    let expected = Body::builder()
-        .add_attribute((
-            "foo",
-            TemplateExpr::QuotedString("bar ${baz} %{~ if cond}qux%{ endif ~}".into()),
-        ))
-        .build();
-
-    let body = parse(input).unwrap();
-
-    assert_eq!(body, expected);
-
-    match body.attributes().next().unwrap().expr() {
-        Expression::TemplateExpr(expr) => {
-            let template = Template::from_expr(expr).unwrap();
-
-            let expected_template = Template::new()
-                .add_literal("bar ")
-                .add_interpolation(Variable::unchecked("baz"))
-                .add_literal(" ")
-                .add_directive(
-                    IfDirective::new(
-                        Variable::unchecked("cond"),
-                        Template::new().add_literal("qux"),
-                    )
-                    .with_if_strip(StripMode::Start)
-                    .with_endif_strip(StripMode::End),
-                );
-
-            assert_eq!(template, expected_template);
-        }
-        _ => unreachable!(),
-    }
-}
diff --git a/src/ser/tests.rs b/src/ser/tests.rs
deleted file mode 100644
index e4047f6..0000000
--- a/src/ser/tests.rs
+++ /dev/null
@@ -1,497 +0,0 @@
-use super::*;
-use crate::expr::{
-    BinaryOp, BinaryOperator, Conditional, Expression, ForExpr, FuncCall, Heredoc,
-    HeredocStripMode, Object, ObjectKey, Operation, RawExpression, TemplateExpr, Traversal,
-    TraversalOperator, Variable,
-};
-use crate::structure::{Attribute, Block, Body};
-use crate::{value, Identifier};
-use pretty_assertions::assert_eq;
-
-#[track_caller]
-fn expect_str<T: Serialize>(value: T, expected: &str) {
-    assert_eq!(to_string(&value).unwrap(), expected);
-}
-
-#[test]
-fn serialize_struct() {
-    #[derive(serde::Serialize)]
-    struct Test {
-        foo: u32,
-        bar: bool,
-    }
-
-    expect_str(Test { foo: 1, bar: true }, "foo = 1\nbar = true\n");
-}
-
-#[test]
-fn serialize_tuple_struct() {
-    #[derive(serde::Serialize)]
-    struct Test1 {
-        foo: u32,
-    }
-
-    #[derive(serde::Serialize)]
-    struct Test2 {
-        bar: &'static str,
-    }
-
-    #[derive(serde::Serialize)]
-    struct TupleStruct(Test1, Test2);
-
-    expect_str(
-        TupleStruct(Test1 { foo: 1 }, Test2 { bar: "baz" }),
-        "foo = 1\nbar = \"baz\"\n",
-    );
-}
-
-#[test]
-fn serialize_enum() {
-    #[derive(serde::Serialize, PartialEq, Debug)]
-    enum E {
-        Unit,
-        Newtype(u32),
-        Tuple(u32, u32),
-        Struct { a: u32 },
-    }
-
-    #[derive(serde::Serialize, PartialEq, Debug)]
-    struct Test {
-        value: E,
-    }
-
-    expect_str(Test { value: E::Unit }, "value = \"Unit\"\n");
-    expect_str(E::Newtype(1), "Newtype = 1\n");
-    expect_str(E::Tuple(1, 2), "Tuple = [\n  1,\n  2\n]\n");
-    expect_str(
-        Test {
-            value: E::Struct { a: 1 },
-        },
-        "value = {\n  \"Struct\" = {\n    \"a\" = 1\n  }\n}\n",
-    );
-}
-
-#[test]
-fn serialize_body() {
-    let value = Body::builder()
-        .add_attribute(("foo", 1u64))
-        .add_attribute(("bar", "baz"))
-        .add_block(
-            Block::builder("qux")
-                .add_attribute(("foo", "bar"))
-                .add_block(
-                    Block::builder("with_labels")
-                        .add_label(Identifier::unchecked("label1"))
-                        .add_label("lab\"el2")
-                        .add_attribute(("baz", vec![1u64, 2u64, 3u64]))
-                        .build(),
-                )
-                .add_attribute(("an_object", {
-                    Object::from([
-                        (
-                            ObjectKey::from(Identifier::unchecked("foo")),
-                            Expression::from("bar"),
-                        ),
-                        (
-                            ObjectKey::from("enabled"),
-                            Expression::from(RawExpression::new("var.enabled")),
-                        ),
-                        (
-                            ObjectKey::Expression(RawExpression::from("var.name").into()),
-                            Expression::from("the value"),
-                        ),
-                    ])
-                }))
-                .build(),
-        )
-        .build();
-
-    let expected = r#"foo = 1
-bar = "baz"
-
-qux {
-  foo = "bar"
-
-  with_labels label1 "lab\"el2" {
-    baz = [
-      1,
-      2,
-      3
-    ]
-  }
-
-  an_object = {
-    foo = "bar"
-    "enabled" = var.enabled
-    var.name = "the value"
-  }
-}
-"#;
-
-    expect_str(value, expected);
-}
-
-#[test]
-fn serialize_object() {
-    let value = value!({
-        foo = [1, 2, 3]
-        bar = "baz"
-        qux = {
-            "foo" = "bar"
-            "baz" = "qux"
-        }
-    });
-
-    let expected = r#"foo = [
-  1,
-  2,
-  3
-]
-bar = "baz"
-qux = {
-  "foo" = "bar"
-  "baz" = "qux"
-}
-"#;
-
-    expect_str(value, expected);
-}
-
-#[test]
-fn serialize_array() {
-    let value = value!([
-        {
-            foo = [1, 2, 3]
-        },
-        {
-            bar = "baz"
-        },
-        {
-            qux = {
-                "foo" = "bar"
-                "baz" = "qux"
-            }
-        }
-    ]);
-
-    let expected = r#"foo = [
-  1,
-  2,
-  3
-]
-bar = "baz"
-qux = {
-  "foo" = "bar"
-  "baz" = "qux"
-}
-"#;
-
-    expect_str(value, expected);
-}
-
-#[test]
-fn serialize_empty_block() {
-    expect_str(Block::builder("empty").build(), "empty {}\n");
-}
-
-#[test]
-fn serialize_traversal() {
-    expect_str(
-        Attribute::new(
-            "attr",
-            Traversal::new(
-                Variable::unchecked("var"),
-                [
-                    TraversalOperator::GetAttr("foo".into()),
-                    TraversalOperator::FullSplat,
-                    TraversalOperator::GetAttr("bar".into()),
-                    TraversalOperator::Index(1u64.into()),
-                    TraversalOperator::AttrSplat,
-                    TraversalOperator::GetAttr("baz".into()),
-                    TraversalOperator::LegacyIndex(42),
-                ],
-            ),
-        ),
-        "attr = var.foo[*].bar[1].*.baz.42\n",
-    );
-}
-
-#[test]
-fn serialize_conditional() {
-    expect_str(
-        Attribute::new(
-            "cond",
-            Conditional::new(Variable::unchecked("cond_var"), "yes", "no"),
-        ),
-        "cond = cond_var ? \"yes\" : \"no\"\n",
-    );
-}
-
-#[test]
-fn serialize_operation() {
-    expect_str(
-        Attribute::new(
-            "op",
-            Operation::Binary(BinaryOp::new(1, BinaryOperator::Plus, 2)),
-        ),
-        "op = 1 + 2\n",
-    );
-}
-
-#[test]
-fn serialize_for_expr() {
-    let body = Body::builder()
-        .add_attribute((
-            "list",
-            ForExpr::new(
-                Identifier::unchecked("item"),
-                Variable::unchecked("items"),
-                FuncCall::builder("func")
-                    .arg(Variable::unchecked("item"))
-                    .build(),
-            )
-            .with_cond_expr(Variable::unchecked("item")),
-        ))
-        .add_attribute((
-            "object",
-            ForExpr::new(
-                Identifier::unchecked("value"),
-                Variable::unchecked("items"),
-                FuncCall::builder("tolower")
-                    .arg(Variable::unchecked("value"))
-                    .build(),
-            )
-            .with_key_var(Identifier::unchecked("key"))
-            .with_key_expr(
-                FuncCall::builder("toupper")
-                    .arg(Variable::unchecked("key"))
-                    .build(),
-            )
-            .with_cond_expr(Operation::Binary(BinaryOp::new(
-                Variable::unchecked("value"),
-                BinaryOperator::NotEq,
-                Expression::Null,
-            )))
-            .with_grouping(true),
-        ))
-        .build();
-
-    let expected = r#"
-list = [for item in items : func(item) if item]
-object = {for key, value in items : toupper(key) => tolower(value)... if value != null}
-"#
-    .trim_start();
-
-    expect_str(body, expected);
-}
-
-#[test]
-fn serialize_func_call() {
-    expect_str(
-        Attribute::new("attr", FuncCall::new("foo")),
-        "attr = foo()\n",
-    );
-    expect_str(
-        Attribute::new("attr", FuncCall::builder("foo").arg(1).arg("two").build()),
-        "attr = foo(1, \"two\")\n",
-    );
-    expect_str(
-        Attribute::new(
-            "attr",
-            FuncCall::builder("foo")
-                .arg(1)
-                .arg(vec!["two", "three"])
-                .expand_final(true)
-                .build(),
-        ),
-        "attr = foo(1, [\"two\", \"three\"]...)\n",
-    );
-}
-
-#[test]
-fn serialize_heredoc() {
-    let body = Body::builder()
-        .add_block(
-            Block::builder("content")
-                .add_attribute((
-                    "heredoc",
-                    TemplateExpr::Heredoc(Heredoc::new(
-                        Identifier::unchecked("HEREDOC"),
-                        "foo\n  bar\nbaz\n",
-                    )),
-                ))
-                .add_attribute((
-                    "heredoc_indent",
-                    TemplateExpr::Heredoc(
-                        Heredoc::new(
-                            Identifier::unchecked("HEREDOC"),
-                            "    foo\n      bar\n    baz\n",
-                        )
-                        .with_strip_mode(HeredocStripMode::Indent),
-                    ),
-                ))
-                .build(),
-        )
-        .build();
-
-    let expected = r#"content {
-  heredoc = <<HEREDOC
-foo
-  bar
-baz
-HEREDOC
-  heredoc_indent = <<-HEREDOC
-    foo
-      bar
-    baz
-  HEREDOC
-}
-"#;
-
-    expect_str(body, expected);
-}
-
-#[test]
-fn serialize_errors() {
-    assert!(to_string(&true).is_err());
-    assert!(to_string("foo").is_err());
-    assert!(to_string(&value!({ "\"" = "invalid attribute name" })).is_err())
-}
-
-#[test]
-fn serialize_with_custom_formatter() {
-    let body = Body::builder()
-        .add_attribute(("foo", 1u64))
-        .add_attribute(("bar", "baz"))
-        .add_block(
-            Block::builder("qux")
-                .add_attribute(("foo", "bar"))
-                .add_block(Block::builder("baz").add_attribute(("qux", true)).build())
-                .add_attribute(("baz", "qux"))
-                .build(),
-        )
-        .build();
-
-    let default_expected = r#"foo = 1
-bar = "baz"
-
-qux {
-  foo = "bar"
-
-  baz {
-    qux = true
-  }
-
-  baz = "qux"
-}
-"#;
-
-    let custom_expected = r#"foo = 1
-bar = "baz"
-qux {
-    foo = "bar"
-    baz {
-        qux = true
-    }
-    baz = "qux"
-}
-"#;
-
-    expect_str(&body, default_expected);
-
-    let mut buf = Vec::new();
-    let formatter = Formatter::builder()
-        .indent(b"    ")
-        .dense(true)
-        .build(&mut buf);
-    let mut ser = Serializer::with_formatter(formatter);
-    ser.serialize(&body).unwrap();
-
-    assert_eq!(String::from_utf8(buf).unwrap(), custom_expected);
-}
-
-#[test]
-fn serialize_nested_expression() {
-    expect_str(
-        Attribute::new(
-            "attr",
-            Expression::Parenthesis(Box::new(Expression::from(Variable::unchecked("foo")))),
-        ),
-        "attr = (foo)\n",
-    );
-}
-
-#[test]
-fn serialize_identifiers_with_hyphens() {
-    expect_str(
-        Attribute::new("hyphen-ated", Expression::Null),
-        "hyphen-ated = null\n",
-    );
-}
-
-#[test]
-fn roundtrip() {
-    let input = Body::builder()
-        .add_block(
-            Block::builder("resource")
-                .add_label("aws_s3_bucket")
-                .add_label("mybucket")
-                .add_attribute((
-                    "count",
-                    Conditional::new(
-                        Traversal::new(Variable::unchecked("var"), ["enabled"]),
-                        1,
-                        0,
-                    ),
-                ))
-                .add_attribute(("bucket", "mybucket"))
-                .add_attribute(("force_destroy", true))
-                .add_block(
-                    Block::builder("server_side_encryption_configuration")
-                        .add_block(
-                            Block::builder("rule")
-                                .add_block(
-                                    Block::builder("apply_server_side_encryption_by_default")
-                                        .add_attribute((
-                                            "kms_master_key_id",
-                                            Traversal::new(
-                                                Variable::unchecked("aws_kms_key"),
-                                                ["mykey", "arn"],
-                                            ),
-                                        ))
-                                        .add_attribute(("sse_algorithm", "aws:kms"))
-                                        .build(),
-                                )
-                                .build(),
-                        )
-                        .build(),
-                )
-                .add_attribute((
-                    "tags",
-                    Expression::from_iter([
-                        (
-                            ObjectKey::from("application"),
-                            Expression::String("myapp".into()),
-                        ),
-                        (
-                            ObjectKey::Identifier("team".into()),
-                            Expression::TemplateExpr(Box::new(TemplateExpr::from("${var.team}"))),
-                        ),
-                        (
-                            ObjectKey::Identifier("environment".into()),
-                            Expression::Parenthesis(Box::new(Expression::from(
-                                Variable::unchecked("environment"),
-                            ))),
-                        ),
-                    ]),
-                ))
-                .build(),
-        )
-        .build();
-
-    let serialized = to_string(&input).unwrap();
-
-    let output: Body = crate::from_str(&serialized).unwrap();
-
-    assert_eq!(input, output);
-}
diff --git a/tests/common/mod.rs b/tests/common/mod.rs
new file mode 100644
index 0000000..ac20d0f
--- /dev/null
+++ b/tests/common/mod.rs
@@ -0,0 +1,73 @@
+#![allow(dead_code)]
+
+use hcl::eval::{Context, Evaluate};
+use hcl::format::{Format, FormatterBuilder};
+use pretty_assertions::assert_eq;
+use serde::{Deserialize, Serialize};
+use std::fmt::Debug;
+
+#[track_caller]
+pub fn assert_serialize<T>(value: T, expected: &str)
+where
+    T: Serialize,
+{
+    assert_eq!(hcl::to_string(&value).unwrap(), expected);
+}
+
+#[track_caller]
+pub fn assert_deserialize<'de, T>(input: &'de str, expected: T)
+where
+    T: Deserialize<'de> + Debug + PartialEq,
+{
+    assert_eq!(hcl::from_str::<T>(input).unwrap(), expected);
+}
+
+#[track_caller]
+pub fn assert_format<T>(value: T, expected: &str)
+where
+    T: Format,
+{
+    assert_eq!(hcl::format::to_string(&value).unwrap(), expected);
+}
+
+#[track_caller]
+pub fn assert_format_builder<'a, T>(builder: FormatterBuilder<'a>, value: T, expected: &str)
+where
+    T: Format,
+{
+    let mut formatter = builder.build_vec();
+    assert_eq!(value.format_string(&mut formatter).unwrap(), expected);
+}
+
+#[track_caller]
+pub fn assert_eval<T, U>(value: T, expected: U)
+where
+    T: Evaluate<Output = U> + Debug + PartialEq,
+    U: Debug + PartialEq,
+{
+    let ctx = Context::new();
+    assert_eq!(value.evaluate(&ctx).unwrap(), expected);
+}
+
+#[track_caller]
+pub fn assert_eval_ctx<T, U>(ctx: &Context, value: T, expected: U)
+where
+    T: Evaluate<Output = U> + Debug + PartialEq,
+    U: Debug + PartialEq,
+{
+    assert_eq!(value.evaluate(ctx).unwrap(), expected);
+}
+
+#[track_caller]
+pub fn assert_eval_error<T, E>(value: T, expected: E)
+where
+    T: Evaluate + Debug + PartialEq,
+    <T as Evaluate>::Output: Debug,
+    E: Into<hcl::eval::Error>,
+{
+    let ctx = Context::new();
+    let err = value.evaluate(&ctx).unwrap_err();
+    let expected = expected.into();
+    assert_eq!(err.kind(), expected.kind());
+    assert_eq!(err.expr(), expected.expr());
+}
diff --git a/tests/custom_blocks.rs b/tests/custom_blocks.rs
index fcd4431..3b93119 100644
--- a/tests/custom_blocks.rs
+++ b/tests/custom_blocks.rs
@@ -1,13 +1,15 @@
+mod common;
+
+use common::assert_serialize;
 use hcl::{
     expr::{BinaryOp, BinaryOperator, Conditional, Expression, Traversal, Variable},
     ser::{block, doubly_labeled_block, labeled_block},
+    Map,
 };
-use indexmap::{indexmap, IndexMap};
-use pretty_assertions::assert_eq;
+use indexmap::indexmap;
+use indoc::indoc;
 use serde::Serialize;
 
-type Map<K, V> = IndexMap<K, V>;
-
 #[test]
 fn custom_block() {
     #[derive(Serialize)]
@@ -20,15 +22,14 @@ fn custom_block() {
         block: indexmap! { "a" => "b", "c" => "d" },
     };
 
-    let expected = r#"
-block {
-  a = "b"
-  c = "d"
-}
-"#
-    .trim_start();
+    let expected = indoc! {r#"
+        block {
+          a = "b"
+          c = "d"
+        }
+    "#};
 
-    assert_eq!(hcl::to_string(&config).unwrap(), expected);
+    assert_serialize(config, expected);
 }
 
 #[test]
@@ -46,18 +47,17 @@ fn custom_labeled_block() {
         },
     };
 
-    let expected = r#"
-block "one" {
-  a = "b"
-}
+    let expected = indoc! {r#"
+        block "one" {
+          a = "b"
+        }
 
-block "two" {
-  c = "d"
-}
-"#
-    .trim_start();
+        block "two" {
+          c = "d"
+        }
+    "#};
 
-    assert_eq!(hcl::to_string(&config).unwrap(), expected);
+    assert_serialize(config, expected);
 }
 
 #[test]
@@ -77,18 +77,17 @@ fn custom_doubly_labeled_block() {
         },
     };
 
-    let expected = r#"
-block "foo" "one" {
-  a = "b"
-}
+    let expected = indoc! {r#"
+        block "foo" "one" {
+          a = "b"
+        }
 
-block "foo" "two" {
-  c = "d"
-}
-"#
-    .trim_start();
+        block "foo" "two" {
+          c = "d"
+        }
+    "#};
 
-    assert_eq!(hcl::to_string(&config).unwrap(), expected);
+    assert_serialize(config, expected);
 }
 
 #[test]
@@ -137,34 +136,33 @@ fn custom_block_structs() {
         },
     };
 
-    let expected = r#"
-a {
-  b = "c"
+    let expected = indoc! {r#"
+        a {
+          b = "c"
 
-  d {
-    e = "f1"
-  }
+          d {
+            e = "f1"
+          }
 
-  d {
-    e = "f2"
-  }
-}
+          d {
+            e = "f2"
+          }
+        }
 
-b "c1" "d" {
-  e = "f3"
-}
+        b "c1" "d" {
+          e = "f3"
+        }
 
-b "c2" "d" {
-  e = "f4"
-}
+        b "c2" "d" {
+          e = "f4"
+        }
 
-b "c2" "d" {
-  e = "f5"
-}
-"#
-    .trim_start();
+        b "c2" "d" {
+          e = "f5"
+        }
+    "#};
 
-    assert_eq!(hcl::to_string(&config).unwrap(), expected);
+    assert_serialize(config, expected);
 }
 
 #[test]
@@ -254,35 +252,34 @@ fn custom_terraform_blocks() {
         },
     };
 
-    let expected = r#"
-resource "aws_s3_bucket" "mybucket" {
-  bucket = "mybucket"
-  force_destroy = true
-
-  server_side_encryption_configuration {
-    rule {
-      apply_server_side_encryption_by_default {
-        kms_master_key_id = aws_kms_key.mykey.arn
-        sse_algorithm = "aws:kms"
-      }
-    }
-  }
-
-  dynamic "kubernetes_network_config" {
-    for_each = var.service_ipv4_cidr == null ? [] : [var.service_ipv4_cidr]
-
-    content {
-      service_ipv4_cidr = kubernetes_network_config.value
-    }
-  }
-
-  tags = {
-    "application" = "myapp"
-    "environment" = "dev"
-  }
-}
-"#
-    .trim_start();
-
-    assert_eq!(hcl::to_string(&config).unwrap(), expected);
+    let expected = indoc! {r#"
+        resource "aws_s3_bucket" "mybucket" {
+          bucket = "mybucket"
+          force_destroy = true
+
+          server_side_encryption_configuration {
+            rule {
+              apply_server_side_encryption_by_default {
+                kms_master_key_id = aws_kms_key.mykey.arn
+                sse_algorithm = "aws:kms"
+              }
+            }
+          }
+
+          dynamic "kubernetes_network_config" {
+            for_each = var.service_ipv4_cidr == null ? [] : [var.service_ipv4_cidr]
+
+            content {
+              service_ipv4_cidr = kubernetes_network_config.value
+            }
+          }
+
+          tags = {
+            "application" = "myapp"
+            "environment" = "dev"
+          }
+        }
+    "#};
+
+    assert_serialize(config, expected);
 }
diff --git a/src/de/tests.rs b/tests/de.rs
similarity index 53%
rename from src/de/tests.rs
rename to tests/de.rs
index c8f3963..5abd3b0 100644
--- a/src/de/tests.rs
+++ b/tests/de.rs
@@ -1,51 +1,86 @@
-use super::*;
-use crate::expr::{
-    BinaryOp, BinaryOperator, Conditional, Expression, ForExpr, FuncCall, Heredoc,
-    HeredocStripMode, ObjectKey, Operation, TemplateExpr, Traversal, TraversalOperator, UnaryOp,
-    UnaryOperator, Variable,
+mod common;
+
+use common::assert_deserialize;
+use hcl::expr::{
+    BinaryOp, BinaryOperator, Expression, ForExpr, FuncCall, Heredoc, HeredocStripMode, ObjectKey,
+    Operation, TemplateExpr, Traversal, UnaryOp, UnaryOperator, Variable,
 };
-use crate::structure::{Block, Body};
-use crate::{value, Identifier, Value};
-use pretty_assertions::assert_eq;
+use hcl::structure::{Block, Body};
+use hcl::{Identifier, Value};
 use serde::Deserialize;
 use std::fmt::Debug;
 
-#[track_caller]
-fn expect_value<'de, T>(input: &'de str, expected: T)
-where
-    T: Deserialize<'de> + Debug + PartialEq,
-{
-    assert_eq!(from_str::<T>(input).unwrap(), expected);
+#[test]
+fn simple() {
+    assert_deserialize(r#"foo = "bar""#, hcl::value!({ foo = "bar" }))
 }
 
 #[test]
-fn deserialize_string_attribute() {
-    expect_value(r#"foo = "bar""#, value!({ foo = "bar" }))
+fn array() {
+    let input = r#"foo = [true, 2, "three", var.enabled]"#;
+    let expected = hcl::value!({ foo = [true, 2, "three", "${var.enabled}"] });
+    assert_deserialize(input, expected);
 }
 
 #[test]
-fn deserialize_object() {
-    expect_value(
+fn object() {
+    assert_deserialize(
         r#"foo = { bar = 42, "baz" = true }"#,
-        value!({ foo = { bar = 42, baz = true } }),
+        hcl::value!({ foo = { bar = 42, baz = true } }),
     )
 }
 
 #[test]
-fn deserialze_block() {
-    expect_value(
+fn custom_struct() {
+    #[derive(Deserialize, PartialEq, Debug)]
+    struct Test {
+        foo: u32,
+    }
+
+    assert_deserialize(r#"foo = 1"#, Test { foo: 1 });
+}
+
+#[test]
+fn custom_enum() {
+    #[derive(Deserialize, PartialEq, Debug)]
+    enum E {
+        Unit,
+        Newtype(u32),
+        Tuple(u32, u32),
+        Struct { a: u32 },
+    }
+
+    #[derive(Deserialize, PartialEq, Debug)]
+    struct Test {
+        value: E,
+    }
+
+    assert_deserialize(r#"value = "Unit""#, Test { value: E::Unit });
+    assert_deserialize(r#"Newtype = 1"#, E::Newtype(1));
+    assert_deserialize(r#"Tuple = [1,2]"#, E::Tuple(1, 2));
+    assert_deserialize(
+        r#"value = {"Struct" = {"a" = 1}}"#,
+        Test {
+            value: E::Struct { a: 1 },
+        },
+    );
+}
+
+#[test]
+fn block() {
+    assert_deserialize(
         r#"resource "aws_s3_bucket" "mybucket" { name = "mybucket" }"#,
-        value!({ resource = { aws_s3_bucket = { mybucket = { name = "mybucket" } } } }),
+        hcl::value!({ resource = { aws_s3_bucket = { mybucket = { name = "mybucket" } } } }),
     );
 
-    expect_value(
+    assert_deserialize(
         r#"block { name = "asdf" }"#,
-        value!({ block = { name = "asdf" } }),
+        hcl::value!({ block = { name = "asdf" } }),
     );
 }
 
 #[test]
-fn deserialize_duplicate_block() {
+fn duplicate_block() {
     let input = r#"
         block {
           foo {
@@ -65,7 +100,8 @@ fn deserialize_duplicate_block() {
           bar = "baz"
         }
     "#;
-    let expected = value!({
+
+    let expected = hcl::value!({
         block = {
             foo = [
                 { "bar" = "baz" },
@@ -85,85 +121,42 @@ fn deserialize_duplicate_block() {
             }
         }
     });
-    expect_value(input, expected);
+    assert_deserialize(input, expected);
 
     let input = r#"
         foo { bar = "baz" }
         foo { bar = 1 }
     "#;
-    let expected = value!({ foo = [{ bar = "baz" }, { bar = 1 }] });
-    expect_value(input, expected);
+    let expected = hcl::value!({ foo = [{ bar = "baz" }, { bar = 1 }] });
+    assert_deserialize(input, expected);
 }
 
 #[test]
-fn deserialize_duplicate_attribute() {
+fn duplicate_attribute() {
     let input = r#"
         foo = ["bar"]
         foo = ["baz"]
     "#;
-    expect_value(input, value!({ foo = ["baz"] }));
+    assert_deserialize(input, hcl::value!({ foo = ["baz"] }));
 }
 
 #[test]
-fn deserialize_duplicate_attribute_and_block() {
+fn duplicate_attribute_and_block() {
     let input = r#"
         foo = ["bar"]
         foo { bar = "baz" }
     "#;
-    expect_value(input, value!({ foo = { bar = "baz" } }));
+    assert_deserialize(input, hcl::value!({ foo = { bar = "baz" } }));
 
     let input = r#"
         foo { bar = "baz" }
         foo = ["bar"]
     "#;
-    expect_value(input, value!({ foo = ["bar"] }));
-}
-
-#[test]
-fn deserialize_tuple() {
-    let input = r#"foo = [true, 2, "three", var.enabled]"#;
-    let expected = value!({ foo = [true, 2, "three", "${var.enabled}"] });
-    expect_value(input, expected);
-}
-
-#[test]
-fn deserialize_struct() {
-    #[derive(Deserialize, PartialEq, Debug)]
-    struct Test {
-        foo: u32,
-    }
-
-    expect_value(r#"foo = 1"#, Test { foo: 1 });
-}
-
-#[test]
-fn deserialize_enum() {
-    #[derive(Deserialize, PartialEq, Debug)]
-    enum E {
-        Unit,
-        Newtype(u32),
-        Tuple(u32, u32),
-        Struct { a: u32 },
-    }
-
-    #[derive(Deserialize, PartialEq, Debug)]
-    struct Test {
-        value: E,
-    }
-
-    expect_value(r#"value = "Unit""#, Test { value: E::Unit });
-    expect_value(r#"Newtype = 1"#, E::Newtype(1));
-    expect_value(r#"Tuple = [1,2]"#, E::Tuple(1, 2));
-    expect_value(
-        r#"value = {"Struct" = {"a" = 1}}"#,
-        Test {
-            value: E::Struct { a: 1 },
-        },
-    );
+    assert_deserialize(input, hcl::value!({ foo = ["bar"] }));
 }
 
 #[test]
-fn deserialize_func_call() {
+fn func_call() {
     let input = r#"attr = foo(1, "bar", ["baz", "qux"]...)"#;
     let expected = Body::builder()
         .add_attribute((
@@ -177,11 +170,11 @@ fn deserialize_func_call() {
         ))
         .build();
 
-    expect_value(input, expected);
+    assert_deserialize(input, expected);
 }
 
 #[test]
-fn deserialize_operation() {
+fn operations() {
     let input = r#"
         unary = !variable
         binary = 1 + 1
@@ -201,11 +194,11 @@ fn deserialize_operation() {
         ))
         .build();
 
-    expect_value(input, expected);
+    assert_deserialize(input, expected);
 }
 
 #[test]
-fn deserialize_for_expr() {
+fn for_exprs() {
     let input = r#"
         list = [for item in items : func(item) if item]
         object = {for key, value in items : toupper(key) => tolower(value)...}
@@ -242,16 +235,151 @@ fn deserialize_for_expr() {
         ))
         .build();
 
-    expect_value(input, expected);
+    assert_deserialize(input, expected);
+}
+
+#[test]
+fn negative_numbers() {
+    let input = r#"
+        float = -4.2
+        float_exp = -4.2e10
+        signed = -42
+    "#;
+
+    let expected = Body::builder()
+        .add_attribute(("float", -4.2f64))
+        .add_attribute(("float_exp", -4.2e10f64))
+        .add_attribute(("signed", -42))
+        .build();
+
+    assert_deserialize(input, expected);
+}
+
+#[test]
+fn template_expr() {
+    let input = r#"foo = "bar ${baz} %{~ if cond}qux%{ endif ~}""#;
+
+    let expected = Body::builder()
+        .add_attribute((
+            "foo",
+            TemplateExpr::QuotedString("bar ${baz} %{~ if cond}qux%{ endif ~}".into()),
+        ))
+        .build();
+
+    assert_deserialize(input, expected);
 }
 
 #[test]
-fn deserialize_invalid_hcl() {
-    assert!(from_str::<Value>(r#"invalid["#).is_err());
+fn null_in_variable_expr() {
+    let input = "foo = null_foo";
+
+    let expected = Body::builder()
+        .add_attribute(("foo", Variable::unchecked("null_foo")))
+        .build();
+
+    assert_deserialize(input, expected);
 }
 
 #[test]
-fn deserialize_terraform() {
+fn object_with_variable_expr_key() {
+    let input = "providers = { aws.eu-central-1 = aws.eu-central-1 }";
+
+    let expected = Body::builder()
+        .add_attribute((
+            "providers",
+            Expression::from_iter([(
+                ObjectKey::Expression(Expression::from(Traversal::new(
+                    Variable::unchecked("aws"),
+                    [Identifier::unchecked("eu-central-1")],
+                ))),
+                Expression::from(Traversal::new(
+                    Variable::unchecked("aws"),
+                    [Identifier::unchecked("eu-central-1")],
+                )),
+            )]),
+        ))
+        .build();
+
+    assert_deserialize(input, expected);
+}
+
+#[test]
+fn traversal_with_expression() {
+    let input =
+        "route_table_id = aws_route_table.private[count.index % var.availability_zone_count].id";
+
+    let expected = Body::builder()
+        .add_attribute((
+            "route_table_id",
+            Traversal::builder(Variable::unchecked("aws_route_table"))
+                .attr("private")
+                .index(BinaryOp::new(
+                    Traversal::builder(Variable::unchecked("count"))
+                        .attr("index")
+                        .build(),
+                    BinaryOperator::Mod,
+                    Traversal::builder(Variable::unchecked("var"))
+                        .attr("availability_zone_count")
+                        .build(),
+                ))
+                .attr("id")
+                .build(),
+        ))
+        .build();
+
+    assert_deserialize(input, expected);
+}
+
+#[test]
+fn unescape_strings() {
+    let input = r#"
+        block "label\\with\\backslashes" {
+          string_attr = "I \u2665 unicode"
+
+          object_attr = {
+            "key\nwith\nnewlines" = true
+          }
+
+          heredoc = <<-EOS
+            heredoc template with \
+            escaped newline and \\backslash is not unescaped yet
+          EOS
+        }
+    "#;
+
+    let expected = Body::builder()
+        .add_block(
+            Block::builder("block")
+                .add_label("label\\with\\backslashes")
+                .add_attribute(("string_attr", "I \u{2665} unicode"))
+                .add_attribute((
+                    "object_attr",
+                    Expression::from_iter([("key\nwith\nnewlines", true)]),
+                ))
+                .add_attribute((
+                    "heredoc",
+                    TemplateExpr::Heredoc(
+                        Heredoc::new(
+                            Identifier::unchecked("EOS"),
+                            "            heredoc template with \\\n            escaped newline and \\\\backslash is not unescaped yet\n"
+                        )
+                        .with_strip_mode(HeredocStripMode::Indent)
+                    )
+                ))
+                .build(),
+        )
+        .build();
+
+    assert_deserialize(input, expected);
+}
+
+#[test]
+fn errors() {
+    assert!(hcl::from_str::<Value>(r#"invalid["#).is_err());
+}
+
+#[test]
+fn terraform() {
     let input = r#"
         resource "aws_s3_bucket" "mybucket" {
           bucket        = "mybucket"
@@ -271,7 +399,8 @@ fn deserialize_terraform() {
             "application" = "myapp"
             team          = "bar"
           }
-        }"#;
+        }
+    "#;
 
     let expected = Body::builder()
         .add_block(
@@ -324,176 +453,5 @@ fn deserialize_terraform() {
         )
         .build();
 
-    expect_value(input, expected);
-}
-
-// https://github.com/martinohmann/hcl-rs/issues/44
-#[test]
-fn issue_44() {
-    use std::collections::HashMap;
-    use std::path::PathBuf;
-
-    #[derive(Deserialize, Debug, PartialEq)]
-    pub struct Config {
-        #[serde(rename = "project")]
-        pub projects: HashMap<String, Project>,
-    }
-
-    #[derive(Deserialize, Debug, PartialEq)]
-    pub struct Project {
-        pub proj_type: String,
-        pub spec: Option<PathBuf>,
-        pub dockerfile: Option<PathBuf>,
-        pub scripts: Option<Vec<Script>>,
-    }
-
-    #[derive(Deserialize, Debug, PartialEq, Eq)]
-    pub struct Script {
-        pub name: String,
-        pub command: String,
-    }
-
-    let input = r#"
-        project "a" {
-            proj_type = "generic"
-            spec = "./test.spec"
-        }
-    "#;
-
-    let expected = Config {
-        projects: {
-            let mut map = HashMap::new();
-            map.insert(
-                "a".into(),
-                Project {
-                    proj_type: "generic".into(),
-                    spec: Some(PathBuf::from("./test.spec")),
-                    dockerfile: None,
-                    scripts: None,
-                },
-            );
-            map
-        },
-    };
-
-    expect_value(input, expected);
-}
-
-#[test]
-fn issue_66() {
-    let expected = Body::builder()
-        .add_attribute((
-            "a",
-            Traversal::new(Variable::unchecked("b"), [Expression::from("c")]),
-        ))
-        .build();
-
-    expect_value("a = b[\"c\"]", expected);
-}
-
-#[test]
-fn issue_81() {
-    let input = r#"
-        attr_splat = module.instance.*.id
-        full_splat = module.instance[*].id
-    "#;
-
-    let expected = Body::builder()
-        .add_attribute((
-            "attr_splat",
-            Traversal::new(
-                Variable::unchecked("module"),
-                [
-                    TraversalOperator::GetAttr("instance".into()),
-                    TraversalOperator::AttrSplat,
-                    TraversalOperator::GetAttr("id".into()),
-                ],
-            ),
-        ))
-        .add_attribute((
-            "full_splat",
-            Traversal::new(
-                Variable::unchecked("module"),
-                [
-                    TraversalOperator::GetAttr("instance".into()),
-                    TraversalOperator::FullSplat,
-                    TraversalOperator::GetAttr("id".into()),
-                ],
-            ),
-        ))
-        .build();
-
-    expect_value(input, expected);
-}
-
-#[test]
-fn issue_83() {
-    let expected = Body::builder()
-        .add_attribute((
-            "attr",
-            Traversal::new(
-                Variable::unchecked("module"),
-                [
-                    TraversalOperator::GetAttr("instance".into()),
-                    TraversalOperator::LegacyIndex(0),
-                    TraversalOperator::GetAttr("id".into()),
-                ],
-            ),
-        ))
-        .build();
-
-    expect_value("attr = module.instance.0.id", expected);
-}
-
-#[test]
-fn issue_137() {
-    #[derive(Deserialize, Debug, PartialEq)]
-    struct Config {
-        expr: Expression,
-        bin_op: BinaryOp,
-        op: Operation,
-        cond: Conditional,
-        cond_string: String,
-        nested: Nested,
-    }
-
-    #[derive(Deserialize, Debug, PartialEq)]
-    struct Nested {
-        template_expr: TemplateExpr,
-        heredoc: Heredoc,
-    }
-
-    let input = r#"
-        expr = 1 + 1
-        bin_op = 1 + 1
-        op = 1 + 1
-        cond = var ? true : false
-        cond_string = var ? true : false
-
-        nested {
-          template_expr = "${some_var}"
-          heredoc = <<-EOS
-            ${some_var}
-          EOS
-        }
-    "#;
-
-    let bin_op = BinaryOp::new(1, BinaryOperator::Plus, 1);
-
-    let expected = Config {
-        expr: Expression::from(bin_op.clone()),
-        bin_op: bin_op.clone(),
-        op: Operation::Binary(bin_op),
-        cond: Conditional::new(Variable::unchecked("var"), true, false),
-        cond_string: String::from("${var ? true : false}"),
-        nested: Nested {
-            template_expr: TemplateExpr::from("${some_var}"),
-            heredoc: Heredoc::new(Identifier::unchecked("EOS"), "            ${some_var}\n")
-                .with_strip_mode(HeredocStripMode::Indent),
-        },
-    };
-
-    let result: Config = crate::from_str(input).unwrap();
-
-    assert_eq!(result, expected);
+    assert_deserialize(input, expected);
 }
diff --git a/src/eval/tests.rs b/tests/eval.rs
similarity index 62%
rename from src/eval/tests.rs
rename to tests/eval.rs
index d208276..6d6fb62 100644
--- a/src/eval/tests.rs
+++ b/tests/eval.rs
@@ -1,64 +1,38 @@
-use super::*;
-use crate::expr::Variable;
-use crate::Number;
-use std::fmt;
-use std::str::FromStr;
-
-#[track_caller]
-fn eval_to_ctx<T, U>(ctx: &Context, value: T, expected: U)
-where
-    T: Evaluate<Output = U> + fmt::Debug + PartialEq,
-    U: fmt::Debug + PartialEq,
-{
-    assert_eq!(value.evaluate(ctx).unwrap(), expected);
-}
-
-#[track_caller]
-fn eval_to<T, U>(value: T, expected: U)
-where
-    T: Evaluate<Output = U> + fmt::Debug + PartialEq,
-    U: fmt::Debug + PartialEq,
-{
-    let ctx = Context::new();
-    eval_to_ctx(&ctx, value, expected);
-}
-
-#[track_caller]
-fn eval_error<T, E>(value: T, expected: E)
-where
-    T: Evaluate + fmt::Debug + PartialEq,
-    <T as Evaluate>::Output: fmt::Debug,
-    E: Into<Error>,
-{
-    let ctx = Context::new();
-    let err = value.evaluate(&ctx).unwrap_err();
-    let expected = expected.into();
-    assert_eq!(err.kind(), expected.kind());
-    assert_eq!(err.expr(), expected.expr());
-}
+mod common;
+
+use common::{assert_eval, assert_eval_ctx, assert_eval_error};
+use hcl::eval::{Context, ErrorKind, EvalResult, FuncArgs, FuncDef, ParamType};
+use hcl::expr::{
+    BinaryOp, BinaryOperator, Conditional, Expression, ForExpr, FuncCall, TemplateExpr, Traversal,
+    TraversalOperator, Variable,
+};
+use hcl::structure::Body;
+use hcl::template::Template;
+use hcl::{Identifier, Number, Value};
+use indoc::indoc;
 
 #[test]
 fn eval_binary_op() {
-    use {BinaryOperator::*, Operation::*};
+    use BinaryOperator::*;
 
-    eval_to(
+    assert_eval(
         BinaryOp::new(
-            Binary(BinaryOp::new(1, Div, 2)),
+            BinaryOp::new(1, Div, 2),
             Mul,
-            Binary(BinaryOp::new(3, Plus, Binary(BinaryOp::new(4, Div, 5)))),
+            BinaryOp::new(3, Plus, BinaryOp::new(4, Div, 5)),
         ),
         Value::from(2.3),
     );
-    eval_to(BinaryOp::new("foo", Eq, "foo"), Value::from(true));
-    eval_to(BinaryOp::new(false, Or, true), Value::from(true));
-    eval_to(BinaryOp::new(true, And, true), Value::from(true));
-    eval_to(BinaryOp::new(true, And, false), Value::from(false));
-    eval_to(BinaryOp::new(1, Less, 2), Value::from(true));
-    eval_to(
+    assert_eval(BinaryOp::new("foo", Eq, "foo"), Value::from(true));
+    assert_eval(BinaryOp::new(false, Or, true), Value::from(true));
+    assert_eval(BinaryOp::new(true, And, true), Value::from(true));
+    assert_eval(BinaryOp::new(true, And, false), Value::from(false));
+    assert_eval(BinaryOp::new(1, Less, 2), Value::from(true));
+    assert_eval(
         BinaryOp::new(
-            Binary(BinaryOp::new(1, Greater, 0)),
+            BinaryOp::new(1, Greater, 0),
             And,
-            Binary(BinaryOp::new("foo", NotEq, Expression::Null)),
+            BinaryOp::new("foo", NotEq, Expression::Null),
         ),
         Value::from(true),
     );
@@ -66,9 +40,9 @@ fn eval_binary_op() {
 
 #[test]
 fn eval_conditional() {
-    eval_to(Conditional::new(true, "yes", "no"), Value::from("yes"));
-    eval_to(Conditional::new(false, "yes", "no"), Value::from("no"));
-    eval_error(
+    assert_eval(Conditional::new(true, "yes", "no"), Value::from("yes"));
+    assert_eval(Conditional::new(false, "yes", "no"), Value::from("no"));
+    assert_eval_error(
         Conditional::new("foo", "yes", "no"),
         ErrorKind::Unexpected(Value::from("foo"), "a boolean"),
     );
@@ -76,25 +50,21 @@ fn eval_conditional() {
 
 #[test]
 fn eval_for_expr() {
-    eval_to(
+    assert_eval(
         ForExpr::new(
             Identifier::unchecked("item"),
             Expression::from_iter([1, 2, 3, 4, 5, 6, 7]),
-            Operation::Binary(BinaryOp::new(
-                Variable::unchecked("item"),
-                BinaryOperator::Mul,
-                2,
-            )),
+            BinaryOp::new(Variable::unchecked("item"), BinaryOperator::Mul, 2),
         )
-        .with_cond_expr(Operation::Binary(BinaryOp::new(
+        .with_cond_expr(BinaryOp::new(
             Variable::unchecked("item"),
             BinaryOperator::Less,
             5,
-        ))),
+        )),
         Value::from_iter([2, 4, 6, 8]),
     );
 
-    eval_to(
+    assert_eval(
         ForExpr::new(
             Identifier::unchecked("value"),
             Expression::from_iter([("a", "1"), ("b", "2"), ("c", "3"), ("d", "4")]),
@@ -102,15 +72,15 @@ fn eval_for_expr() {
         )
         .with_key_var(Identifier::unchecked("key"))
         .with_key_expr(Variable::unchecked("value"))
-        .with_cond_expr(Operation::Binary(BinaryOp::new(
+        .with_cond_expr(BinaryOp::new(
             Variable::unchecked("key"),
             BinaryOperator::NotEq,
             Expression::from("d"),
-        ))),
+        )),
         Value::from_iter([("1", "a"), ("2", "b"), ("3", "c")]),
     );
 
-    eval_to(
+    assert_eval(
         ForExpr::new(
             Identifier::unchecked("value"),
             Expression::from_iter(["a", "b", "c", "d"]),
@@ -121,7 +91,7 @@ fn eval_for_expr() {
         Value::from_iter([("0", "a"), ("1", "b"), ("2", "c"), ("3", "d")]),
     );
 
-    eval_to(
+    assert_eval(
         ForExpr::new(
             Identifier::unchecked("value"),
             Expression::from_iter([("a", "1"), ("b", "2"), ("c", "3"), ("d", "4")]),
@@ -131,7 +101,7 @@ fn eval_for_expr() {
         Value::from_iter(["a", "b", "c", "d"]),
     );
 
-    eval_to(
+    assert_eval(
         ForExpr::new(
             Identifier::unchecked("value"),
             Expression::from_iter(["a", "b", "c", "d"]),
@@ -142,7 +112,7 @@ fn eval_for_expr() {
         Value::from_iter([("0", "a"), ("1", "b"), ("2", "c"), ("3", "d")]),
     );
 
-    eval_to(
+    assert_eval(
         ForExpr::new(
             Identifier::unchecked("value"),
             Expression::from_iter([("a", 1), ("b", 2), ("c", 3), ("d", 4)]),
@@ -150,16 +120,16 @@ fn eval_for_expr() {
         )
         .with_key_var(Identifier::unchecked("key"))
         .with_key_expr(Expression::from("foo"))
-        .with_cond_expr(Operation::Binary(BinaryOp::new(
+        .with_cond_expr(BinaryOp::new(
             Variable::unchecked("key"),
             BinaryOperator::NotEq,
             Expression::from("d"),
-        )))
+        ))
         .with_grouping(true),
         Value::from_iter([("foo", vec![1, 2, 3])]),
     );
 
-    eval_error(
+    assert_eval_error(
         ForExpr::new(
             Identifier::unchecked("v"),
             Expression::from_iter(["a"]),
@@ -175,31 +145,31 @@ fn eval_traversal() {
     use TraversalOperator::*;
 
     // legacy index access: expr.2
-    eval_to(
+    assert_eval(
         Traversal::new(vec![1, 2, 3], [LegacyIndex(1)]),
         Value::from(2),
     );
 
     // legacy index access: expr[2]
-    eval_to(
+    assert_eval(
         Traversal::new(vec![1, 2, 3], [Index(Expression::from(2))]),
         Value::from(3),
     );
 
     // get-attr: expr.foo
-    eval_to(
+    assert_eval(
         Traversal::new(
-            expression!({"foo" = [1, 2, 3], "bar" = []}),
+            hcl::expression!({"foo" = [1, 2, 3], "bar" = []}),
             [GetAttr(Identifier::unchecked("foo"))],
         ),
         Value::from_iter([1, 2, 3]),
     );
 
     // chain get-attr -> index: expr.foo[2]
-    eval_to(
+    assert_eval(
         Traversal::new(
             Traversal::new(
-                expression!({"foo" = [1, 2, 3], "bar" = []}),
+                hcl::expression!({"foo" = [1, 2, 3], "bar" = []}),
                 [GetAttr(Identifier::unchecked("foo"))],
             ),
             [Index(Expression::from(2))],
@@ -208,18 +178,18 @@ fn eval_traversal() {
     );
 
     // full-splat non-array
-    eval_to(
+    assert_eval(
         Traversal::new(
-            expression!({"foo" = [1, 2, 3], "bar" = []}),
+            hcl::expression!({"foo" = [1, 2, 3], "bar" = []}),
             [FullSplat, GetAttr(Identifier::unchecked("foo"))],
         ),
         Value::from_iter([vec![1, 2, 3]]),
     );
 
     // full-splat array
-    eval_to(
+    assert_eval(
         Traversal::new(
-            expression! {
+            hcl::expression! {
                 [
                     { "foo" = 2 },
                     { "foo" = 1, "bar" = 2 }
@@ -231,7 +201,7 @@ fn eval_traversal() {
     );
 
     // full-splat null
-    eval_to(
+    assert_eval(
         Traversal::new(
             Expression::Null,
             [FullSplat, GetAttr(Identifier::unchecked("foo"))],
@@ -240,18 +210,18 @@ fn eval_traversal() {
     );
 
     // attr-splat non-array
-    eval_to(
+    assert_eval(
         Traversal::new(
-            expression!({"foo" = [1, 2, 3], "bar" = []}),
+            hcl::expression!({"foo" = [1, 2, 3], "bar" = []}),
             [AttrSplat, GetAttr(Identifier::unchecked("foo"))],
         ),
         Value::from_iter([vec![1, 2, 3]]),
     );
 
     // attr-splat array
-    eval_to(
+    assert_eval(
         Traversal::new(
-            expression! {
+            hcl::expression! {
                 [
                     { "foo" = 2 },
                     { "foo" = 1, "bar" = 2 }
@@ -263,7 +233,7 @@ fn eval_traversal() {
     );
 
     // attr-splat null
-    eval_to(
+    assert_eval(
         Traversal::new(
             Expression::Null,
             [AttrSplat, GetAttr(Identifier::unchecked("foo"))],
@@ -272,9 +242,9 @@ fn eval_traversal() {
     );
 
     // attr-splat followed by non-get-attr
-    eval_to(
+    assert_eval(
         Traversal::new(
-            expression! {
+            hcl::expression! {
                 [
                     { "foo" = { "bar" = [1, 2, 3] } },
                     { "foo" = { "bar" = [10, 20, 30] } }
@@ -284,16 +254,16 @@ fn eval_traversal() {
                 AttrSplat,
                 GetAttr(Identifier::unchecked("foo")),
                 GetAttr(Identifier::unchecked("bar")),
-                Index(expression!(1)),
+                Index(hcl::expression!(1)),
             ],
         ),
         Value::from_iter([10, 20, 30]),
     );
 
     // full-splat followed by non-get-attr
-    eval_to(
+    assert_eval(
         Traversal::new(
-            expression! {
+            hcl::expression! {
                 [
                     { "foo" = { "bar" = [1, 2, 3] } },
                     { "foo" = { "bar" = [10, 20, 30] } }
@@ -303,14 +273,14 @@ fn eval_traversal() {
                 FullSplat,
                 GetAttr(Identifier::unchecked("foo")),
                 GetAttr(Identifier::unchecked("bar")),
-                Index(expression!(1)),
+                Index(hcl::expression!(1)),
             ],
         ),
         Value::from_iter([2, 20]),
     );
 
     // errors
-    eval_error(
+    assert_eval_error(
         Traversal::new(vec![1, 2, 3], [LegacyIndex(5)]),
         ErrorKind::Index(5),
     );
@@ -340,7 +310,7 @@ fn eval_func_call() {
         FuncDef::builder().param(ParamType::String).build(strlen),
     );
 
-    eval_to_ctx(
+    assert_eval_ctx(
         &ctx,
         FuncCall::builder("add")
             .arg(FuncCall::builder("strlen").arg("foo").build())
@@ -352,50 +322,56 @@ fn eval_func_call() {
 
 #[test]
 fn eval_template() {
+    use std::str::FromStr;
+
     let mut ctx = Context::new();
     ctx.declare_var("name", "World");
 
-    eval_to_ctx(
+    assert_eval_ctx(
         &ctx,
         Template::from_str("Hello, ${name ~} !").unwrap(),
         String::from("Hello, World!"),
     );
 
-    let template_str = r#"Let's ${~ what ~} :
-%{ for item in items ~}
-- ${item}
-%{~ endfor ~}
+    let template_str = indoc! {r#"
+        Let's ${~ what ~} :
+        %{ for item in items ~}
+        - ${item}
+        %{~ endfor ~}
 
-"#;
+    "#};
 
-    let expected = r#"Let's render a list:
-- foo
-- bar
-- baz"#;
+    let expected = indoc! {r#"
+        Let's render a list:
+        - foo
+        - bar
+        - baz
+    "#}
+    .trim_end();
 
     let mut ctx = Context::new();
     ctx.declare_var("what", " render a list");
     ctx.declare_var("items", vec!["foo", "bar", "baz"]);
 
-    eval_to_ctx(
+    assert_eval_ctx(
         &ctx,
         Template::from_str(template_str).unwrap(),
-        String::from(expected),
+        expected.to_owned(),
     );
 }
 
 #[test]
 fn expr_error_context() {
-    let input = r#"
+    let input = indoc! {r#"
         block {
             attr = cond ? "yes" : "no"
         }
-    "#;
+    "#};
 
     // The `cond` variable is not defined which should forcefully fail the evaluation.
     let ctx = Context::new();
 
-    let err = from_str::<Body>(input, &ctx).unwrap_err();
+    let err = hcl::eval::from_str::<Body>(input, &ctx).unwrap_err();
 
     assert_eq!(
         err.to_string(),
@@ -406,76 +382,39 @@ fn expr_error_context() {
 #[test]
 fn interpolation_unwrapping() {
     // unwrapping
-    eval_to(TemplateExpr::from("${null}"), Value::Null);
-    eval_to(TemplateExpr::from("${\"foo\"}"), Value::from("foo"));
-    eval_to(TemplateExpr::from("${true}"), Value::Bool(true));
-    eval_to(TemplateExpr::from("${\"${true}\"}"), Value::Bool(true));
-    eval_to(
+    assert_eval(TemplateExpr::from("${null}"), Value::Null);
+    assert_eval(TemplateExpr::from("${\"foo\"}"), Value::from("foo"));
+    assert_eval(TemplateExpr::from("${true}"), Value::Bool(true));
+    assert_eval(TemplateExpr::from("${\"${true}\"}"), Value::Bool(true));
+    assert_eval(
         TemplateExpr::from("${42}"),
         Value::Number(Number::from(42u64)),
     );
-    eval_to(
+    assert_eval(
         TemplateExpr::from("${1.5}"),
         Value::Number(Number::from_f64(1.5).unwrap()),
     );
-    eval_to(
+    assert_eval(
         TemplateExpr::from("${[1, 2, 3]}"),
         Value::from_iter([1, 2, 3]),
     );
-    eval_to(
+    assert_eval(
         TemplateExpr::from("${{ a = 1, b = 2 }}"),
         Value::from_iter([("a", 1), ("b", 2)]),
     );
 
     let mut ctx = Context::new();
     ctx.declare_var("var", true);
-    eval_to_ctx(&ctx, TemplateExpr::from("${\"${var}\"}"), Value::Bool(true));
+    assert_eval_ctx(&ctx, TemplateExpr::from("${\"${var}\"}"), Value::Bool(true));
 
     // no unwrapping
-    eval_to(
+    assert_eval(
         TemplateExpr::from("hello ${true}"),
         Value::from("hello true"),
     );
-    eval_to(TemplateExpr::from("${\"\"}${true}"), Value::from("true"));
-    eval_to(
+    assert_eval(TemplateExpr::from("${\"\"}${true}"), Value::from("true"));
+    assert_eval(
         TemplateExpr::from("%{ for v in [true] }${v}%{ endfor }"),
         Value::from("true"),
     );
 }
-
-#[test]
-fn param_type() {
-    let string = Value::from("a string");
-    let number = Value::from(42);
-    let boolean = Value::from(true);
-    let string_array = Value::from_iter(["foo", "bar"]);
-    let number_array = Value::from_iter([1, 2, 3]);
-    let object_of_strings = Value::from_iter([("foo", "bar"), ("baz", "qux")]);
-    let object_of_numbers = Value::from_iter([("foo", 1), ("bar", 2)]);
-
-    let param = ParamType::String;
-    assert!(param.is_satisfied_by(&string));
-    assert!(!param.is_satisfied_by(&number));
-
-    let param = ParamType::Any;
-    assert!(param.is_satisfied_by(&string));
-    assert!(param.is_satisfied_by(&number));
-
-    let param = ParamType::nullable(ParamType::String);
-    assert!(param.is_satisfied_by(&string));
-    assert!(param.is_satisfied_by(&Value::Null));
-    assert!(!param.is_satisfied_by(&number));
-
-    let param = ParamType::one_of([ParamType::String, ParamType::Number]);
-    assert!(param.is_satisfied_by(&string));
-    assert!(param.is_satisfied_by(&number));
-    assert!(!param.is_satisfied_by(&boolean));
-
-    let param = ParamType::array_of(ParamType::String);
-    assert!(param.is_satisfied_by(&string_array));
-    assert!(!param.is_satisfied_by(&number_array));
-
-    let param = ParamType::object_of(ParamType::String);
-    assert!(param.is_satisfied_by(&object_of_strings));
-    assert!(!param.is_satisfied_by(&object_of_numbers));
-}
diff --git a/tests/format.rs b/tests/format.rs
new file mode 100644
index 0000000..4b883b7
--- /dev/null
+++ b/tests/format.rs
@@ -0,0 +1,285 @@
+mod common;
+
+use common::{assert_format, assert_format_builder};
+use hcl::expr::{
+    BinaryOp, BinaryOperator, Conditional, Expression, ForExpr, FuncCall, Heredoc,
+    HeredocStripMode, Traversal, TraversalOperator, Variable,
+};
+use hcl::format::Formatter;
+use hcl::template::{ForDirective, IfDirective, StripMode, Template};
+use hcl::Identifier;
+use indoc::indoc;
+
+#[test]
+fn prefer_ident_keys() {
+    let attr = hcl::body!({
+        object = {
+            "foo" = 1
+            bar = 2
+            "baz qux" = 3
+        }
+    });
+
+    assert_format_builder(
+        Formatter::builder().prefer_ident_keys(false),
+        &attr,
+        indoc! {r#"
+            object = {
+              "foo" = 1
+              bar = 2
+              "baz qux" = 3
+            }
+        "#},
+    );
+
+    assert_format_builder(
+        Formatter::builder().prefer_ident_keys(true),
+        attr,
+        indoc! {r#"
+            object = {
+              foo = 1
+              bar = 2
+              "baz qux" = 3
+            }
+        "#},
+    );
+}
+
+#[test]
+fn compact_arrays() {
+    assert_format_builder(
+        Formatter::builder().compact_arrays(true),
+        hcl::body!({ array = [1, 2, 3, [4, 5]] }),
+        indoc! {r#"
+            array = [1, 2, 3, [4, 5]]
+        "#},
+    );
+}
+
+#[test]
+fn compact_objects() {
+    assert_format_builder(
+        Formatter::builder().compact_objects(true),
+        hcl::body!({
+            object = {
+                foo = {
+                    bar = "baz"
+                }
+                qux = "bam"
+            }
+        }),
+        indoc! {r#"
+            object = { foo = { bar = "baz" }, qux = "bam" }
+        "#},
+    );
+}
+
+#[test]
+fn compact_func_args() {
+    assert_format(
+        FuncCall::builder("func")
+            .arg(vec![1, 2, 3])
+            .arg(hcl::expression!({
+                foo = "bar"
+                baz = "qux"
+            }))
+            .build(),
+        indoc! {r#"
+            func([1, 2, 3], { foo = "bar", baz = "qux" })
+        "#}
+        .trim_end(),
+    );
+}
+
+#[test]
+fn func_call_expand_final() {
+    assert_format(
+        FuncCall::builder("func")
+            .arg(1)
+            .arg(vec!["two", "three"])
+            .expand_final(true)
+            .build(),
+        indoc! {r#"
+            func(1, ["two", "three"]...)
+        "#}
+        .trim_end(),
+    );
+}
+
+#[test]
+fn for_list_expr() {
+    assert_format(
+        ForExpr::new(
+            Identifier::unchecked("item"),
+            Variable::unchecked("items"),
+            FuncCall::builder("func")
+                .arg(Variable::unchecked("item"))
+                .build(),
+        )
+        .with_cond_expr(Variable::unchecked("item")),
+        "[for item in items : func(item) if item]",
+    );
+}
+
+#[test]
+fn for_object_expr() {
+    assert_format(
+        ForExpr::new(
+            Identifier::unchecked("value"),
+            Variable::unchecked("items"),
+            FuncCall::builder("tolower")
+                .arg(Variable::unchecked("value"))
+                .build(),
+        )
+        .with_key_var(Identifier::unchecked("key"))
+        .with_key_expr(
+            FuncCall::builder("toupper")
+                .arg(Variable::unchecked("key"))
+                .build(),
+        )
+        .with_cond_expr(BinaryOp::new(
+            Variable::unchecked("value"),
+            BinaryOperator::NotEq,
+            Expression::Null,
+        ))
+        .with_grouping(true),
+        "{for key, value in items : toupper(key) => tolower(value)... if value != null}",
+    );
+}
+
+#[test]
+fn conditional() {
+    assert_format(
+        Conditional::new(Variable::unchecked("cond_var"), "yes", "no"),
+        r#"cond_var ? "yes" : "no""#,
+    );
+}
+
+#[test]
+fn operation() {
+    assert_format(BinaryOp::new(1, BinaryOperator::Plus, 2), "1 + 2");
+}
+
+#[test]
+fn template() {
+    let template = Template::new().add_directive(
+        ForDirective::new(
+            Identifier::unchecked("item"),
+            Variable::unchecked("items"),
+            Template::new()
+                .add_literal("\nHello ")
+                .add_directive(
+                    IfDirective::new(
+                        BinaryOp::new(Variable::unchecked("item"), BinaryOperator::Eq, "world"),
+                        Template::new().add_literal(" World! "),
+                    )
+                    .with_false_template(
+                        Template::new()
+                            .add_literal(" ")
+                            .add_interpolation(Variable::unchecked("item"))
+                            .add_literal("."),
+                    )
+                    .with_if_strip(StripMode::Start)
+                    .with_else_strip(StripMode::Both)
+                    .with_endif_strip(StripMode::End),
+                )
+                .add_literal("\n"),
+        )
+        .with_for_strip(StripMode::End)
+        .with_endfor_strip(StripMode::End),
+    );
+
+    let expected = indoc! {r#"
+        %{ for item in items ~}
+        Hello %{~ if item == "world" } World! %{~ else ~} ${item}.%{ endif ~}
+        %{ endfor ~}
+    "#}
+    .trim_end();
+
+    assert_format(template, expected);
+}
+
+#[test]
+fn parenthesis() {
+    assert_format(
+        Expression::Parenthesis(Box::new(Variable::unchecked("foo").into())),
+        "(foo)",
+    );
+}
+
+#[test]
+fn heredoc() {
+    let body = hcl::body!({
+        content {
+            heredoc = (
+                Heredoc::new(
+                    Identifier::unchecked("HEREDOC"),
+                    "foo\n  bar\nbaz\n"
+                )
+            )
+        }
+    });
+
+    let expected = indoc! {r#"
+        content {
+          heredoc = <<HEREDOC
+        foo
+          bar
+        baz
+        HEREDOC
+        }
+    "#};
+
+    assert_format(body, expected);
+}
+
+#[test]
+fn indented_heredoc() {
+    let body = hcl::body!({
+        content {
+            heredoc_indent = (
+                Heredoc::new(
+                    Identifier::unchecked("HEREDOC"),
+                    "    foo\n      bar\n    baz\n",
+                )
+                .with_strip_mode(HeredocStripMode::Indent)
+            )
+        }
+    });
+
+    let expected = indoc! {r#"
+        content {
+          heredoc_indent = <<-HEREDOC
+            foo
+              bar
+            baz
+          HEREDOC
+        }
+    "#};
+
+    assert_format(body, expected);
+}
+
+#[test]
+fn traversal() {
+    assert_format(
+        Traversal::new(
+            Variable::unchecked("var"),
+            [
+                TraversalOperator::GetAttr("foo".into()),
+                TraversalOperator::FullSplat,
+                TraversalOperator::GetAttr("bar".into()),
+                TraversalOperator::Index(1u64.into()),
+                TraversalOperator::AttrSplat,
+                TraversalOperator::GetAttr("baz".into()),
+                TraversalOperator::LegacyIndex(42),
+            ],
+        ),
+        "var.foo[*].bar[1].*.baz.42",
+    );
+}
+
+#[test]
+fn empty_block() {
+    assert_format(hcl::block!(empty {}), "empty {}\n");
+}
diff --git a/tests/regressions.rs b/tests/regressions.rs
new file mode 100644
index 0000000..e7917d2
--- /dev/null
+++ b/tests/regressions.rs
@@ -0,0 +1,203 @@
+mod common;
+
+use common::{assert_deserialize, assert_format};
+use hcl::{expr::*, Identifier};
+use indoc::indoc;
+use serde::Deserialize;
+use std::collections::HashMap;
+use std::path::PathBuf;
+
+// https://github.com/martinohmann/hcl-rs/issues/44
+#[test]
+fn issue_44() {
+    #[derive(Deserialize, Debug, PartialEq)]
+    pub struct Config {
+        #[serde(rename = "project")]
+        pub projects: HashMap<String, Project>,
+    }
+
+    #[derive(Deserialize, Debug, PartialEq)]
+    pub struct Project {
+        pub proj_type: String,
+        pub spec: Option<PathBuf>,
+        pub dockerfile: Option<PathBuf>,
+        pub scripts: Option<Vec<Script>>,
+    }
+
+    #[derive(Deserialize, Debug, PartialEq, Eq)]
+    pub struct Script {
+        pub name: String,
+        pub command: String,
+    }
+
+    let expected = Config {
+        projects: {
+            let mut map = HashMap::new();
+            map.insert(
+                "a".into(),
+                Project {
+                    proj_type: "generic".into(),
+                    spec: Some(PathBuf::from("./test.spec")),
+                    dockerfile: None,
+                    scripts: None,
+                },
+            );
+            map
+        },
+    };
+
+    let input = indoc! {r#"
+        project "a" {
+            proj_type = "generic"
+            spec = "./test.spec"
+        }
+    "#};
+
+    assert_deserialize(input, expected);
+}
+
+// https://github.com/martinohmann/hcl-rs/issues/66
+#[test]
+fn issue_66() {
+    let expected =
+        hcl::body!({ a = (Traversal::new(Variable::unchecked("b"), [Expression::from("c")])) });
+
+    assert_deserialize(r#"a = b["c"]"#, expected);
+}
+
+// https://github.com/martinohmann/hcl-rs/issues/81
+#[test]
+fn issue_81() {
+    let var = Variable::unchecked("module");
+
+    let expected = hcl::body!({
+        attr_splat = (Traversal::builder(var.clone()).attr("instance").attr_splat().attr("id").build())
+        full_splat = (Traversal::builder(var).attr("instance").full_splat().attr("id").build())
+    });
+
+    let input = indoc! {r#"
+        attr_splat = module.instance.*.id
+        full_splat = module.instance[*].id
+    "#};
+
+    assert_deserialize(input, expected);
+}
+
+// https://github.com/martinohmann/hcl-rs/issues/83
+#[test]
+fn issue_83() {
+    let expected = hcl::body!({
+        attr = (Traversal::new(
+            Variable::unchecked("module"),
+            [
+                TraversalOperator::GetAttr("instance".into()),
+                TraversalOperator::LegacyIndex(0),
+                TraversalOperator::GetAttr("id".into()),
+            ],
+        ))
+    });
+
+    assert_deserialize("attr = module.instance.0.id", expected);
+}
+
+// https://github.com/martinohmann/hcl-rs/issues/137
+#[test]
+fn issue_137() {
+    #[derive(Deserialize, Debug, PartialEq)]
+    struct Config {
+        expr: Expression,
+        bin_op: BinaryOp,
+        op: Operation,
+        cond: Conditional,
+        cond_string: String,
+        nested: Nested,
+    }
+
+    #[derive(Deserialize, Debug, PartialEq)]
+    struct Nested {
+        template_expr: TemplateExpr,
+        heredoc: Heredoc,
+    }
+
+    let bin_op = BinaryOp::new(1, BinaryOperator::Plus, 1);
+
+    let expected = Config {
+        expr: Expression::from(bin_op.clone()),
+        bin_op: bin_op.clone(),
+        op: Operation::Binary(bin_op),
+        cond: Conditional::new(Variable::unchecked("var"), true, false),
+        cond_string: String::from("${var ? true : false}"),
+        nested: Nested {
+            template_expr: TemplateExpr::from("${some_var}"),
+            heredoc: Heredoc::new(Identifier::unchecked("EOS"), "    ${some_var}\n")
+                .with_strip_mode(HeredocStripMode::Indent),
+        },
+    };
+
+    let input = indoc! {r#"
+        expr = 1 + 1
+        bin_op = 1 + 1
+        op = 1 + 1
+        cond = var ? true : false
+        cond_string = var ? true : false
+
+        nested {
+          template_expr = "${some_var}"
+          heredoc = <<-EOS
+            ${some_var}
+          EOS
+        }
+    "#};
+
+    assert_deserialize(input, expected);
+}
+
+// https://github.com/martinohmann/hcl-rs/issues/87
+#[test]
+fn issue_87() {
+    let expr = Expression::from(
+        FuncCall::builder("foo")
+            .arg(Expression::from_iter([("bar", FuncCall::new("baz"))]))
+            .build(),
+    );
+
+    assert_format(
+        expr,
+        indoc! {r#"
+            foo({ "bar" = baz() })
+        "#}
+        .trim_end(),
+    );
+}
+
+// https://github.com/martinohmann/hcl-rs/issues/91
+#[test]
+fn issue_91() {
+    assert_format(
+        hcl::attribute!(_foo = "bar"),
+        indoc! {r#"
+            _foo = "bar"
+        "#},
+    );
+}
+
+// https://github.com/martinohmann/hcl-rs/issues/131
+#[test]
+fn issue_131() {
+    assert_format(
+        hcl::attribute!(a = (TemplateExpr::from(r#"${"b"}"#))),
+        indoc! {r#"
+            a = "${"b"}"
+        "#},
+    );
+
+    assert_format(
+        hcl::value!({ a = r#"${"b"}"# }),
+        indoc! {r#"
+            {
+              "a" = "${"b"}"
+            }
+        "#}
+        .trim_end(),
+    );
+}
diff --git a/tests/ser.rs b/tests/ser.rs
new file mode 100644
index 0000000..7faa349
--- /dev/null
+++ b/tests/ser.rs
@@ -0,0 +1,201 @@
+mod common;
+
+use common::assert_serialize;
+use hcl::expr::{Expression, RawExpression};
+use hcl::structure::Attribute;
+use indoc::indoc;
+
+#[test]
+fn custom_struct() {
+    #[derive(serde::Serialize)]
+    struct Test {
+        foo: u32,
+        bar: bool,
+    }
+
+    assert_serialize(
+        Test { foo: 1, bar: true },
+        indoc! {r#"
+            foo = 1
+            bar = true
+        "#},
+    );
+}
+
+#[test]
+fn custom_tuple_struct() {
+    #[derive(serde::Serialize)]
+    struct Test1 {
+        foo: u32,
+    }
+
+    #[derive(serde::Serialize)]
+    struct Test2 {
+        bar: &'static str,
+    }
+
+    #[derive(serde::Serialize)]
+    struct TupleStruct(Test1, Test2);
+
+    assert_serialize(
+        TupleStruct(Test1 { foo: 1 }, Test2 { bar: "baz" }),
+        indoc! {r#"
+            foo = 1
+            bar = "baz"
+        "#},
+    );
+}
+
+#[test]
+fn custom_enum() {
+    #[derive(serde::Serialize, PartialEq, Debug)]
+    enum E {
+        Unit,
+        Newtype(u32),
+        Tuple(u32, u32),
+        Struct { a: u32 },
+    }
+
+    #[derive(serde::Serialize, PartialEq, Debug)]
+    struct Test {
+        value: E,
+    }
+
+    assert_serialize(Test { value: E::Unit }, "value = \"Unit\"\n");
+    assert_serialize(E::Newtype(1), "Newtype = 1\n");
+    assert_serialize(
+        E::Tuple(1, 2),
+        indoc! {r#"
+            Tuple = [
+              1,
+              2
+            ]
+        "#},
+    );
+    assert_serialize(
+        Test {
+            value: E::Struct { a: 1 },
+        },
+        indoc! {r#"
+            value = {
+              "Struct" = {
+                "a" = 1
+              }
+            }
+        "#},
+    );
+}
+
+#[test]
+fn body() {
+    let value = hcl::body!({
+        foo = 1
+        bar = "baz"
+
+        qux {
+          foo = "bar"
+
+          with_labels label1 "lab\"el2" {
+            baz = [
+              1,
+              2,
+              3
+            ]
+          }
+
+          an_object = {
+            foo = "bar"
+            "enabled" = (RawExpression::new("var.enabled"))
+            (RawExpression::from("var.name")) = "the value"
+          }
+        }
+    });
+
+    let expected = indoc! {r#"
+        foo = 1
+        bar = "baz"
+
+        qux {
+          foo = "bar"
+
+          with_labels label1 "lab\"el2" {
+            baz = [
+              1,
+              2,
+              3
+            ]
+          }
+
+          an_object = {
+            foo = "bar"
+            "enabled" = var.enabled
+            var.name = "the value"
+          }
+        }
+    "#};
+
+    assert_serialize(value, expected);
+}
+
+#[test]
+fn object() {
+    let value = hcl::value!({
+        foo = [1, 2, 3]
+        bar = "baz"
+        qux = { "foo" = "bar", "baz" = "qux" }
+    });
+
+    let expected = indoc! {r#"
+        foo = [
+          1,
+          2,
+          3
+        ]
+        bar = "baz"
+        qux = {
+          "foo" = "bar"
+          "baz" = "qux"
+        }
+    "#};
+
+    assert_serialize(value, expected);
+}
+
+#[test]
+fn array() {
+    let value = hcl::value!([
+        { foo = [1, 2, 3] },
+        { bar = "baz" },
+        { qux = { "foo" = "bar", "baz" = "qux" } }
+    ]);
+
+    let expected = indoc! {r#"
+        foo = [
+          1,
+          2,
+          3
+        ]
+        bar = "baz"
+        qux = {
+          "foo" = "bar"
+          "baz" = "qux"
+        }
+    "#};
+
+    assert_serialize(value, expected);
+}
+
+#[test]
+fn invalid_top_level_types() {
+    assert!(hcl::to_string(&true).is_err());
+    assert!(hcl::to_string("foo").is_err());
+    assert!(hcl::to_string(&hcl::value!({ "\"" = "invalid attribute name" })).is_err())
+}
+
+#[test]
+fn identifiers_with_hyphens() {
+    assert_serialize(
+        Attribute::new("hyphen-ated", Expression::Null),
+        "hyphen-ated = null\n",
+    );
+}
diff --git a/tests/template.rs b/tests/template.rs
new file mode 100644
index 0000000..5417fde
--- /dev/null
+++ b/tests/template.rs
@@ -0,0 +1,23 @@
+use hcl::expr::{TemplateExpr, Variable};
+use hcl::template::{IfDirective, StripMode, Template};
+use pretty_assertions::assert_eq;
+
+#[test]
+fn from_expr() {
+    let expr = TemplateExpr::from("bar ${baz} %{~ if cond}qux%{ endif ~}");
+
+    let expected = Template::new()
+        .add_literal("bar ")
+        .add_interpolation(Variable::unchecked("baz"))
+        .add_literal(" ")
+        .add_directive(
+            IfDirective::new(
+                Variable::unchecked("cond"),
+                Template::new().add_literal("qux"),
+            )
+            .with_if_strip(StripMode::Start)
+            .with_endif_strip(StripMode::End),
+        );
+
+    assert_eq!(Template::from_expr(&expr).unwrap(), expected);
+}