Comparing objects in terraform test

I am trying to make an assertion about a complex object in terraform test. I am finding it very difficult because there seems to be no way to construct an object in Terraform.

I am trying to assert that a local is a specific object of type

  object(
    {
      name                      = string
      database                  = optional(string)
      superuser                 = optional(bool, false)
      create_database           = optional(bool, false)
      create_role               = optional(bool, false)
      inherit                   = optional(bool, true)
      login                     = optional(bool, true)
      replication               = optional(bool, false)
      connection_limit          = optional(number, -1)
      encrypted_password        = optional(bool, true)
      bypass_row_level_security = optional(bool, false)
      valid_until               = optional(string, "infinity")
      roles                     = optional(list(string))
      search_path               = optional(list(string))
      schema                    = optional(string, "public")
      with_grant_option         = optional(string, false)
      database_privileges       = optional(list(string), ["CONNECT", "CREATE", "TEMPORARY"])
      table_privileges          = optional(list(string), ["SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE", "REFERENCES", "TRIGGER"])
      sequence_privileges       = optional(list(string), ["USAGE", "SELECT", "UPDATE"])
      revoke_public             = optional(bool, false)
      password_rotation         = optional(bool, false)
    }
  )

In the end I’ve come up with this:

run "test_locals__roles_set" {
  command = plan

  variables {
    roles = [{
      name     = "example_role"
      database = "example_db"
    }]
  }

 assert {
   condition = jsonencode(local.roles_set) == jsonencode({
     example_role = {
       name                      = "example_role"
       database                  = "example_db"
       superuser                 = false
       create_database           = false
       create_role               = false
       inherit                   = true
       login                     = true
       replication               = false
       connection_limit          = -1
       encrypted_password        = true
       bypass_row_level_security = false
       valid_until               = "infinity"
       roles                     = null
       search_path               = null
       schema                    = "public"
       with_grant_option         = "false"
       database_privileges       = ["CONNECT", "CREATE", "TEMPORARY"]
       table_privileges          = ["SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE", "REFERENCES", "TRIGGER"]
       sequence_privileges       = ["USAGE", "SELECT", "UPDATE"]
       revoke_public             = false
       password_rotation         = false
       index                     = 0
     }
   })
   error_message = "Unexpected local.roles_set."
 }
}

Is there any better way to achieve this than what I’ve done here?

Hi @alexharv074,

Comparing the JSON serializations of the objects does seem like a pragmatic approach if the level of detail that JSON can preserve is sufficient for what you’re trying to test. In particular, JSON encoding erases the distinction between list, tuple, and set, between map and object, and between null values of different types.

If you want to do it without jsonencode then you would need to match the attribute types so that the equality test doesn’t immediately fail due to type mismatch. That means using explicit type conversion functions for types where Terraform doesn’t have any direct syntax (and so would normally rely on automatic type conversion, but cannot here).

Unfortunately there are some situations in this example that Terraform currently has no syntax to deal with because it would normally rely on automatic type conversion. In particular, there’s no short expression syntax for writing a null value of type list(string), because tolist(null) doesn’t give any information about the last element type, so would produce list(any) instead.

I guess one tricky way to force it would be to use a conditional expression to force Terraform to deduce the correct result type by example:

false ? local.roles_set : {
  # (all of your attributes)
}

A conditional expression uses type inference to find a common base type that both results can convert to, and so in this case it would force Terraform to automatically convert the nulls to match the types in local.roles_set. i don’t love that answer because it is using a conditional expression for something other than its intended purpose, but if that works then we could think about offering a more intentional “type inference by example” function that would make it clearer what’s going on, or perhaps even something that’s specialized for comparing complex types in a way that uses the type constraint of one value to infer types of the other value all in one step.

@apparentlymart Is it fair to say the terraform test framework has given rise to a need for some more functions for type conversions? Using jsonencode does feel like the least worst of options available but still feels also like a bit of an abuse of that function where we rely on a side effect rather than its intended use case. Should I raise an issue so that we have something in the backlog?

A lot of test frameworks have some sort of way to directly compare lists / maps / etc, as well as pretty print diffs when things don’t line up.

In my tests so far, terraform doesn’t seem to always compare the way you’d expect, even if the types are the same.

assert {
    condition = google_compute_backend_bucket.foo.custom_response_headers = [
      "X-Foo: 1",
      "X-Bar: 2",
    ]
}

The comparison works properly if I jsonencode() both sides, as mentioned before, but without it, you get an unhelpful message:

│ Error: Test assertion failed
│ 
│   on tests/main.tftest.hcl line 123, in run "basic":
│  123:     condition = google_compute_backend_bucket.cdn_backend_bucket.custom_response_headers == [
│  124:       "X-Foo: 1",
│  125:       "X-Bar: 2",
│  126:     ]
│     ├────────────────
│     │ google_compute_backend_bucket.foo.custom_response_headers is list of string with 2 elements

As best I can tell, it’s not due to sort order or formatting, and if you directly assert each element of the array by index, it works as you’d expect.

It would be nice to see both sides of the comparison (even when they’re dissimilar), and it would be very nice if there were a builtin way to compare equality / deep equality of lists / maps / etc.

I opened Better comparison of data structures in assertions · Issue #35760 · hashicorp/terraform · GitHub