Repository: ovh/venom

Issue:
Add ShouldNotJSONEqual
Hi!

I think it would be super useful to have the negation of `ShouldJSONEqual` as `ShouldNotJSONEqual`.

I need to check if a JSON has changed from one step to the next one, and as it is now I can only check the Equality. I think is easy enough to implement, and it can be very useful (same as the other negations ShouldNotJSONContain or ShouldNotJSONContainWithKey).

Thanks in advance :) 

Discussion:

Solution patch:
diff --git a/README.md b/README.md
index 80b232c..6f95143 100644
--- a/README.md
+++ b/README.md
@@ -10,43 +10,52 @@ Venom is a CLI (Command Line Interface) that aims to create, manage and run your
  
 # Table of content
 
-* [Overview](#overview)
-* [Installing](#installing)
-* [Updating](#updating)
-* [Docker image](#docker-image)
-* [CLI usage](#cli-usage)
-  * [Globstar support](#globstar-support)
-  * [Variables](#variables)
-    * [Variable Definitions Files](#variable-definitions-files)
-    * [Environment Variables](#environment-variables)
-  * [Arguments](#arguments)
-    * [Define arguments with environment variables](#define-arguments-with-environment-variables)
-    * [Use a configuration file](#use-a-configuration-file)
-* [Concepts](#concepts)
-  * [TestSuites](#testsuites)
-  * [Executors](#executors)
-    * [User defined executors](#user-defined-executors)
-  * [Variables](#variables)
-    * [Testsuite variables](#testsuite-variables)
-    * [Variable helpers](#variable-helpers)
-  * [Use outputs from a test step as input of another test step](#use-outputs-from-a-test-step-as-input-of-another-test-step)
-  * [Builtin venom variables](#builtin-venom-variables)
-  * [Assertions](#assertions)
-    * [Keywords](#keywords)
-      * [`Must` Keywords](#must-keywords)
-    * [Using logical operators](#using-logical-operators)
-* [Write and run your first test suite](#write-and-run-your-first-test-suite)
-* [Export tests report](#export-tests-report)
-* [Advanced usage](#advanced-usage)
-  * [Debug your testsuites](#debug-your-testsuites)
-  * [Skip testcase and teststeps](#skip-testcase-and-teststeps)
-  * [Iterating over data](#iterating-over-data)
-* [FAQ](#faq)
-  * [Common errors with quotes](#common-errors-with-quotes)
-* [Use venom in CI/CD pipelines](#use-venom-in-cicd-pipelines)
-* [Hacking](#hacking)
-* [Contributing](#contributing)
-* [License](#license)
+- [🐍 Venom](#-venom)
+- [Table of content](#table-of-content)
+- [Overview](#overview)
+- [Installing](#installing)
+  - [Install from binaries](#install-from-binaries)
+- [Updating](#updating)
+- [Docker image](#docker-image)
+- [CLI Usage](#cli-usage)
+  - [Run test suites in a specific order](#run-test-suites-in-a-specific-order)
+  - [Globstar support](#globstar-support)
+  - [Variables](#variables)
+    - [Variable Definitions Files](#variable-definitions-files)
+    - [Environment Variables](#environment-variables)
+  - [Arguments](#arguments)
+    - [Define arguments with environment variables](#define-arguments-with-environment-variables)
+  - [Use a configuration file](#use-a-configuration-file)
+- [Concepts](#concepts)
+  - [TestSuites](#testsuites)
+  - [Executors](#executors)
+    - [User defined executors](#user-defined-executors)
+  - [Variables](#variables-1)
+    - [Testsuite variables](#testsuite-variables)
+    - [Variable helpers](#variable-helpers)
+  - [Use outputs from a test step as input of another test step](#use-outputs-from-a-test-step-as-input-of-another-test-step)
+  - [Builtin venom variables](#builtin-venom-variables)
+    - [Secrets variables](#secrets-variables)
+  - [Assertions](#assertions)
+    - [Keywords](#keywords)
+      - [`Must` keywords](#must-keywords)
+    - [Using logical operators](#using-logical-operators)
+- [Write and run your first test suite](#write-and-run-your-first-test-suite)
+- [Export tests report](#export-tests-report)
+- [Advanced usage](#advanced-usage)
+  - [Debug your testsuites](#debug-your-testsuites)
+  - [Skip testcase and teststeps](#skip-testcase-and-teststeps)
+  - [Iterating over data](#iterating-over-data)
+- [FAQ](#faq)
+  - [Common errors with quotes](#common-errors-with-quotes)
+- [Use venom in CI/CD pipelines](#use-venom-in-cicd-pipelines)
+- [Hacking](#hacking)
+  - [How to compile?](#how-to-compile)
+  - [How to test?](#how-to-test)
+    - [Unit tests:](#unit-tests)
+    - [Integration tests:](#integration-tests)
+- [Contributing](#contributing)
+- [License](#license)
 
 # Overview
 
@@ -670,6 +679,7 @@ The value `this-value-is-secret` will not be printed in your console, `venom.log
 * ShouldTimeEqual - [example](https://github.com/ovh/venom/tree/master/tests/assertions/ShouldTimeEqual.yml)
 * ShouldMatchRegex - [example](https://github.com/ovh/venom/tree/master/tests/assertions/ShouldMatchRegex.yml)
 * ShouldJSONEqual - [example](https://github.com/ovh/venom/tree/master/tests/assertions/ShouldJSONEqual.yml)
+* ShouldNotJSONEqual - [example](https://github.com/ovh/venom/tree/master/tests/assertions/ShouldNotJSONEqual.yml)
 
 #### `Must` keywords
 
diff --git a/assertions/assertions.go b/assertions/assertions.go
index 9b864f0..50fce3a 100644
--- a/assertions/assertions.go
+++ b/assertions/assertions.go
@@ -66,6 +66,7 @@ var assertMap = map[string]AssertFunc{
 	"ShouldHappenBetween":          ShouldHappenBetween,
 	"ShouldTimeEqual":              ShouldTimeEqual,
 	"ShouldJSONEqual":              ShouldJSONEqual,
+	"ShouldNotJSONEqual":           ShouldNotJSONEqual,
 	"ShouldBeArray":                ShouldBeArray,
 	"ShouldBeMap":                  ShouldBeMap,
 	"ShouldMatchRegex":             ShouldMatchRegex,
@@ -1473,6 +1474,18 @@ func ShouldJSONEqual(actual interface{}, expected ...interface{}) error {
 	}
 }
 
+// ShouldNotJSONEqual asserts that the actual value is not JSON equal to the expected value
+func ShouldNotJSONEqual(actual interface{}, expected ...interface{}) error {
+	if err := need(1, expected); err != nil {
+		return err
+	}
+
+	if err := ShouldJSONEqual(actual, expected...); err == nil {
+		return fmt.Errorf("expected %v to not be JSON equals to %v", actual, expected[0])
+	}
+	return nil
+}
+
 func getTimeFromString(in interface{}) (time.Time, error) {
 	if t, isTime := in.(time.Time); isTime {
 		return t, nil



Test patch:
diff --git a/assertions/assertions_test.go b/assertions/assertions_test.go
index 847cdf4..accbc8e 100644
--- a/assertions/assertions_test.go
+++ b/assertions/assertions_test.go
@@ -2329,3 +2329,112 @@ func TestShouldJSONEqual(t *testing.T) {
 		})
 	}
 }
+
+func TestShouldNotJSONEqual(t *testing.T) {
+	tests := []struct {
+		name     string
+		actual   interface{}
+		expected interface{}
+		wantErr  bool
+	}{
+		{
+			name:     "equal objects should error",
+			actual:   map[string]interface{}{"a": 1, "b": 2},
+			expected: `{"a": 1, "b": 2}`,
+			wantErr:  true,
+		},
+		{
+			name:     "different objects should not error",
+			actual:   map[string]interface{}{"a": 1, "b": 2},
+			expected: `{"a": 1, "b": 3}`,
+			wantErr:  false,
+		},
+		{
+			name:     "equal arrays should error",
+			actual:   []interface{}{1, 2, 3},
+			expected: `[1, 2, 3]`,
+			wantErr:  true,
+		},
+		{
+			name:     "different arrays should not error",
+			actual:   []interface{}{1, 2, 3},
+			expected: `[1, 2, 4]`,
+			wantErr:  false,
+		},
+		{
+			name:     "equal strings should error",
+			actual:   "test",
+			expected: "test",
+			wantErr:  true,
+		},
+		{
+			name:     "different strings should not error",
+			actual:   "test1",
+			expected: "test2",
+			wantErr:  false,
+		},
+		{
+			name:     "equal numbers should error",
+			actual:   json.Number("123"),
+			expected: json.Number("123"),
+			wantErr:  true,
+		},
+		{
+			name:     "different numbers should not error",
+			actual:   json.Number("123"),
+			expected: json.Number("456"),
+			wantErr:  false,
+		},
+		{
+			name:     "equal booleans should error",
+			actual:   true,
+			expected: true,
+			wantErr:  true,
+		},
+		{
+			name:     "different booleans should not error",
+			actual:   true,
+			expected: false,
+			wantErr:  false,
+		},
+		{
+			name:     "null values should error",
+			actual:   "",
+			expected: "null",
+			wantErr:  true,
+		},
+		{
+			name:     "null and non-null should not error",
+			actual:   "",
+			expected: "not null",
+			wantErr:  false,
+		},
+		{
+			name:     "objects with different key order should error",
+			actual:   map[string]interface{}{"a": 1, "b": 2},
+			expected: `{"b": 2, "a": 1}`,
+			wantErr:  true,
+		},
+		{
+			name:     "nested objects should error when equal",
+			actual:   map[string]interface{}{"a": map[string]interface{}{"b": 1}},
+			expected: `{"a": {"b": 1}}`,
+			wantErr:  true,
+		},
+		{
+			name:     "nested objects should not error when different",
+			actual:   map[string]interface{}{"a": map[string]interface{}{"b": 1}},
+			expected: `{"a": {"b": 2}}`,
+			wantErr:  false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := ShouldNotJSONEqual(tt.actual, tt.expected)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("ShouldNotJSONEqual() error = %v, wantErr %v", err, tt.wantErr)
+			}
+		})
+	}
+}
diff --git a/tests/assertions/ShouldNotJSONEqual.yml b/tests/assertions/ShouldNotJSONEqual.yml
new file mode 100644
index 0000000..d3e7ad7
--- /dev/null
+++ b/tests/assertions/ShouldNotJSONEqual.yml
@@ -0,0 +1,31 @@
+name: test ShouldNotJSONEqual
+testcases:
+- name: test assertion ShouldNotJSONEqual with different values (should pass)
+  steps:
+  - type: exec
+    script: |
+      echo '{
+        "o" : {
+          "a" : 1,
+          "b" : 2,
+          "c" : {
+              "x":1,
+              "y":2
+          }
+        },
+        "a" : [1,2],
+        "s" : "foo",
+        "n" : 1.2,
+        "t" : true,
+        "f" : false,
+        "z" : null
+      }'
+    assertions:
+      - result.systemoutjson.o ShouldNotJSONEqual '{"a":1,"b":3,"c":{"x":1,"y":2}}'
+      - result.systemoutjson.o.c ShouldNotJSONEqual '{"x":2,"y":2}'
+      - result.systemoutjson.a ShouldNotJSONEqual '[1,3]'
+      - result.systemoutjson.s ShouldNotJSONEqual 'bar'
+      - result.systemoutjson.n ShouldNotJSONEqual 1.3
+      - result.systemoutjson.t ShouldNotJSONEqual false
+      - result.systemoutjson.f ShouldNotJSONEqual true
+      - result.systemoutjson.z ShouldNotJSONEqual 'not null'