1

I have aproblem with testing my unzip method.

My unit test method is like:

[Fact]
        public void UnzipData_CalledWithByteArrayParameter_ReturnsString()
        {
            _serializationService.CallerName = "";
            byte[] array = new byte[] { 31, 139, 8, 0, 0, 0, 0, 0, 0, 11, 117, 205, 49, 15, 130, 48, 16, 134, 255, 255, 114, 43, 148, 220, 157, 165, 45, 55, 234, 204, 98, 216, 140, 3, 9, 23, 37, 209, 18, 67, 53, 38, 134, 255, 46, 168, 139, 3, 243, 247, 189, 121, 14, 47, 232, 159, 32, 148, 195, 110, 136, 73, 99, 170, 135, 78, 47, 32, 47, 136, 237, 85, 65, 160, 214, 211, 185, 141, 144, 195, 163, 31, 251, 52, 126, 174, 93, 155, 150, 137, 145, 130, 65, 103, 152, 26, 44, 165, 172, 4, 109, 65, 155, 210, 57, 79, 25, 178, 32, 206, 213, 114, 221, 235, 237, 174, 99, 210, 238, 175, 129, 105, 202, 191, 56, 175, 226, 219, 97, 13, 174, 12, 178, 97, 219, 80, 16, 14, 98, 185, 32, 31, 188, 117, 62, 67, 90, 133, 127, 205, 12, 31, 223, 154, 207, 196, 62, 247, 0, 0, 0 };

            string result = _serializationService.UnzipData(array);

            Assert.False(result.Length == 0);
            Assert.True(result.Length > array.Length);
        }

but result variable is null. Sut based on this answer:

public string UnzipData(byte[] bytes)
        {
            LoadDictionaries();
            CallerName = _messageService.GetCallerName();

            using (var msi = new MemoryStream(bytes))
            using (var mso = new MemoryStream())
            {
                try
                {
                    using (var gs = new GZipStream(msi, CompressionMode.Decompress))
                    {
                        CopyTo(gs, mso);
                    }

                    return Encoding.UTF8.GetString(mso.ToArray());
                }
                catch (Exception e)
                {
                    _logger.Error(e, MessagesError[CallerName]);

                    return null;
                }
            }
        }

and

public static void CopyTo(Stream src, Stream dest)
        {
            byte[] bytes = new byte[4096];

            int cnt;

            while ((cnt = src.Read(bytes, 0, bytes.Length)) != 0)
            {
                dest.Write(bytes, 0, cnt);
            }
        }

The main problem is that I am not quite sure how to pass byte[] compressed by GZipStream data in my unit test.

As I belive, in test preparation I am not supposed to do something like this? Is not it breaking testability?

[Fact]
        public void UnzipData_CalledWithByteArrayParameter_ReturnsString()
        {
            _serializationService.CallerName = "";
            string someString = "fdfsdfsdfsdfsf";
            byte[] array = _serializationService.ZipData(someString); //using compress method first????

            string result = _serializationService.UnzipData(array);

            Assert.False(result.Length == 0);
            Assert.True(result.Length > array.Length);
        }

Or maybe I can?

ZipData method:

public byte[] ZipData(string data)
        {
            LoadDictionaries();
            CallerName = _messageService.GetCallerName();
            var bytes = Encoding.UTF8.GetBytes(data);

            using (var msi = new MemoryStream(bytes))
            using (var mso = new MemoryStream())
            {
                try
                {
                    using (var gs = new GZipStream(mso, CompressionMode.Compress))
                    {
                        CopyTo(msi, gs);
                    }

                    byte[] toReturn = mso.ToArray();

                    return toReturn;
                }
                catch (Exception e)
                {
                    _logger.Error(e, MessagesError[CallerName]);

                    return null;
                }
            }
        }

And compressing method returns me:

new byte[] { 31, 139, 8, 0, 0, 0, 0, 0, 0, 11, 125, 206, 61, 11, 194, 48, 16, 6, 224, 255, 114, 171, 77, 185, 94, 62, 74, 179, 234, 234, 34, 221, 196, 33, 144, 27, 2, 53, 197, 38, 138, 80, 250, 223, 141, 90, 16, 151, 194, 45, 7, 247, 222, 243, 158, 103, 8, 79, 176, 77, 5, 251, 49, 102, 142, 249, 56, 122, 30, 192, 206, 16, 221, 149, 193, 194, 193, 69, 7, 21, 120, 151, 223, 27, 97, 99, 4, 106, 129, 166, 39, 178, 100, 202, 212, 168, 100, 139, 170, 219, 33, 189, 196, 245, 244, 196, 183, 59, 167, 204, 254, 47, 3, 203, 182, 125, 61, 218, 240, 166, 129, 35, 151, 63, 143, 144, 66, 78, 159, 110, 63, 93, 9, 236, 4, 233, 30, 181, 45, 5, 164, 172, 165, 54, 70, 181, 155, 250, 154, 41, 250, 229, 5, 222, 237, 15, 25, 239, 0, 0, 0 }

from string:

[{\"ix\":1,\"ContentModel\":{\"name\":\"Dana\",\"date\":\"2016-05-06T22:26:26.0437049+02:00\",\"dateRequested\":\"2016-05-06\"}},{\"ix\":2,\"ContentModel\":{\"name\":\"Darlene\",\"visits\":1,\"date\":\"2014-09-25T05:22:33.3566479+02:00\",\"dateRequested\":\"2014-09-25\"}}]

UPDATED TEST

[Fact]
        public void UnzipData_CalledWithByteArrayParameter_ReturnsString()
        {
            //string _jsonExample i converted in txtwizard.net/compression
            string _jsonExample = "[{\"ix\":1,\"ContentModel\":{\"name\":\"Dana\",\"date\":\"2016-05-06T22:26:26.0437049+02:00\",\"dateRequested\":\"2016-05-06\"}},{\"ix\":2,\"ContentModel\":{\"name\":\"Darlene\",\"visits\":1,\"date\":\"2014-09-25T05:22:33.3566479+02:00\",\"dateRequested\":\"2014-09-25\"}}]";
            var base64String = "H4sIAAAAAAAA/4WOPwvCMBDFv0tWm3K9/CnNqquLdDMOgdwQqCmaKIL0uxu1irgUbjjevXfvt79bFm6WmaaybD3GTDFvR09DkcopuiOVzbKNi86y4vEuvxWERnNQHHSPaFCXqUGKFmS3AjQAX/uOThdKmfxfzrJpqj79uNR/HijS6+c1pJDTzPzLIzl0HFUPyhQkIWqhtJbtIs+ce/IcHmtZn30RAQAA";
            byte[] array = Convert.FromBase64String(base64String);

            _serializationService.CallerName = "";

            string result = _serializationService.UnzipData(array);

            Assert.Equals(result, _jsonExample); //not equal, added extra backslashes
            Assert.Equal("SomeMethod", _serializationService.CallerName);
            _messageServiceMock.Verify(m => m.GetCallerName(It.IsAny<string>()), Times.Once);
        }
  • 1
    If it’s null obviously an error happened and the code returns null from the exception handler. So the code doesn’t work. Check the exception and fix? – Sami Kuhmonen Aug 04 '19 at 06:17
  • @SamiKuhmonen Fair andswer. But is it allright if I will use another method for compressing the string in my test method before testing decompression? `byte[] array = _serializationService.ZipData(someString);` and then `string result = _serializationService.UnzipData(array);` –  Aug 04 '19 at 08:00
  • @Przemyslaw.Pszemek Yes it is ok to use another method on the subject to zip the data. If that other subject method has its own test then it is also covered if it fails to behave as expected. – Nkosi Aug 04 '19 at 11:33
  • @Przemyslaw.Pszemek also provided you are using a more recent version of .net, then you can do `gs.CopyTo(mso)` instead of using that local `CopyTo(,)` function. – Nkosi Aug 04 '19 at 11:37
  • @Nkosi do you think that it will be still unit test and will not break isolation rule, if `ZipData` is also covered by unit tests? –  Aug 04 '19 at 12:21
  • 1
    @Przemyslaw.Pszemek In my *opinion* zipping the data is part of arranging the test. The fact that you have to use a member of the subject in this case does not break isolation since the zip is not what is being tested. You could easily have written the zip code again manually to ensure you have correct data but why repeat existing functionality. That would not be very DRY, would it. – Nkosi Aug 04 '19 at 12:23
  • @Nkosi ok, thank you for oppinion. Currently I am still trying to pass manually `byte[]` returned by `ZipData` to `UnzipData` in the test, but in unit test `result` remains **null**, because of some reason, so maybe I will stick to just zipping it in the test. –  Aug 04 '19 at 12:28
  • @Przemyslaw.Pszemek include your zip code. I'll take a look at your implementation and test and see if I notice anything. – Nkosi Aug 04 '19 at 12:30
  • @Nkosi added code and return result –  Aug 04 '19 at 12:38

2 Answers2

2

Your source data in:-

byte[] array = new byte[] { 31, 139, 8, 0, 0, 0, 0, 0, 0, 11, 117, 205, 49, 15, 130, 48, 16, 134, 255, 255, 114, 43, 148, 220, 157, 165, 45, 55, 234, 204, 98, 216, 140, 3, 9, 23, 37, 209, 18, 67, 53, 38, 134, 255, 46, 168, 139, 3, 243, 247, 189, 121, 14, 47, 232, 159, 32, 148, 195, 110, 136, 73, 99, 170, 135, 78, 47, 32, 47, 136, 237, 85, 65, 160, 214, 211, 185, 141, 144, 195, 163, 31, 251, 52, 126, 174, 93, 155, 150, 137, 145, 130, 65, 103, 152, 26, 44, 165, 172, 4, 109, 65, 155, 210, 57, 79, 25, 178, 32, 206, 213, 114, 221, 235, 237, 174, 99, 210, 238, 175, 129, 105, 202, 191, 56, 175, 226, 219, 97, 13, 174, 12, 178, 97, 219, 80, 16, 14, 98, 185, 32, 31, 188, 117, 62, 67, 90, 133, 127, 205, 12, 31, 223, 154, 207, 196, 62, 247, 0, 0, 0 };

Is not valid Gzip data, and your method is throwing an InvalidDataException where you are returning null.

Otherwise, your method works fine with correct input data.

Aleks
  • 1,629
  • 14
  • 19
  • So is it ok to compress string first with another method? `byte[] array = _serializationService.ZipData(someString);` –  Aug 04 '19 at 07:27
  • If you're unit testing, it's probably better to have some known good compressed data to test each of your compress and uncompress methods separately, in addition to having a test which includes both. I find the best way store/pass byte arrays in tests is to have Base64 encoded data rather than defining a byte[] with the values in the definition. After a quick search, I found a site http://www.txtwizard.net/compression that you could use to generate your test data. The results it provides are already in Base64 format. – Aleks Aug 04 '19 at 22:35
  • Thank you for the tip, I gave a try yesterday with this website. The problem is that my `ZipData` method output is in a format like `new byte[] { 31, 139, 8, 0, 0, 0, 0, 0, 0, 11, 125, 206, 61, 11, 194, 48, 16, 6, 224, 255, 114, 171, 77, 185, 94, 62, 74, 179, 234, 234, 34, 221, 196, 33, 144, 27, 2, 53, 197, 38, 138, 80, 250, 223, 141, 90, 16, 151, 194, 45, 7, 247, 222, 243, 158, 103, 8, 79, 176, 77, 5, 251, 49, 102, 142, 249, 56, 122, 30, 192, 206... }` etc. And in the website I receive `H4sIAAAAAAAA/4WOPwvCMBDFv0tWm3K9/CnNqquLdDMOgdwQqCmaKIL0uxu1irgUbjjevXfvt79bFD3G...` or something similar –  Aug 05 '19 at 07:53
  • 1
    It's Base64; use `Convert.FromBase64String` to convert it to a byte array, or `Convert.ToBase64String` to convert from a byte array to a string (like the website uses) – Aleks Aug 05 '19 at 08:36
  • OK! This is what I was looking for. Only problem now is that when using **FromBase64String** it adds extra backslashes, like here: `"[{\\\"ix\\\":1,\\\"ContentModel\\\":{\\\"` instead of this: `"[{\"ix\":1,\"ContentModel\":{\"` –  Aug 05 '19 at 08:55
  • I'd need to see the code you are using. It looks like some step you are taking is adding escaping to the JSON string you are using. Per your original question, provided the Compress and Decompress methods work OK with plain text (or preferably a `byte[]`, and leave the encoding outside your Compress/Decompress methods), then your GZip tests will work OK. If you are then having issues with converting JSON to a byte array (e.g. with added escaping, then that's a separate issue) – Aleks Aug 05 '19 at 23:00
  • Updated question with code that I am using. But I am guessing that maybe compression method used on the website might cause the problem with additional backslashes? –  Aug 06 '19 at 00:42
  • 1
    It is possible that the website is further escaping the code you are pasting in. I suggest keeping your test data simple in order to keep your tests isolated (otherwise you're trying to test too many things in a single unit). You also don't have to use that website, it was just a suggestion for sourcing data. You could just run your Compress/Decompress methods manually with some test data; if the plaintext matches after it's been through both methods, then at least you can be sure the methods work with each other. – Aleks Aug 06 '19 at 00:49
  • Allright, I used **Lorem ipsum...** on the website and it was compressed correctly. It was a good point. –  Aug 06 '19 at 00:56
  • Great. So did this answer your question? – Aleks Aug 06 '19 at 01:07
1

Using the core parts of your provided code to create a minimal example of the subject under test

public class Subject {

    public byte[] ZipData(string data) {
        //LoadDictionaries();
        //CallerName = _messageService.GetCallerName();
        var bytes = Encoding.UTF8.GetBytes(data);

        using (var msi = new MemoryStream(bytes))
        using (var mso = new MemoryStream()) {
            try {
                using (var gs = new GZipStream(mso, CompressionMode.Compress)) {
                    msi.CopyTo(gs);
                }
                byte[] toReturn = mso.ToArray();
                return toReturn;
            } catch (Exception e) {
                // _logger.Error(e, MessagesError[CallerName]);
                return Array.Empty<byte>();
            }
        }
    }

    public string UnzipData(byte[] bytes) {
        //LoadDictionaries();
        //CallerName = _messageService.GetCallerName();
        using (var msi = new MemoryStream(bytes))
        using (var mso = new MemoryStream()) {
            try {
                using (var gs = new GZipStream(msi, CompressionMode.Decompress)) {
                    gs.CopyTo(mso);
                }
                return Encoding.UTF8.GetString(mso.ToArray());
            } catch (Exception e) {
                //_logger.Error(e, MessagesError[CallerName]);
                return string.Empty;
            }
        }
    }
}

The following test behaves as expected when exercised

[TestClass]
public class MyTestClass {
    [TestMethod]
    public void UnzipData_CalledWithByteArrayParameter_ReturnsString() {
        //Arrange
        var _serializationService = new Subject();
        string expected = "[{\"ix\":1,\"ContentModel\":{\"name\":\"Dana\",\"date\":\"2016-05-06T22:26:26.0437049+02:00\",\"dateRequested\":\"2016-05-06\"}},{\"ix\":2,\"ContentModel\":{\"name\":\"Darlene\",\"visits\":1,\"date\":\"2014-09-25T05:22:33.3566479+02:00\",\"dateRequested\":\"2014-09-25\"}}]";
        byte[] array = _serializationService.ZipData(expected);

        //Act
        string actual = _serializationService.UnzipData(array);

        //Assert
        actual.Should().NotBeNull()
            .And.Be(expected);            
    }
}

Note the changes made to the used functions. Mainly not returning null which can bring its own complications.

Your assertions were also in accurate since the string and byte array lengths wont match given the zip process.

In my opinion zipping the data as part of arranging the test does not break isolation since the zip is not what is being tested (technically).

You could easily have rewritten the zip code again manually in the test to ensure you have correct data to supply to the member under test but why repeat existing functionality.

That would not be very DRY.

The assumption here would be that the zip functionality would have also had its own isolated unit test. If its test failed then that member is already covered.

Nkosi
  • 235,767
  • 35
  • 427
  • 472
  • I was just wondering, are you able to figure out, why my `UnzipData` method when taking `new byte[] { 31, 139, 8, 0, 0, 0, 0, 0, 0, 11, 125, 206, 61, 11 ...}` is throwing exception and when taking data from `ZipData` all is fine? –  Aug 05 '19 at 08:24
  • @update: **Aleks** from comments gave me a solution that I was looking for, where I can *ARRANGE* a `byte[]` in my unit test with using `base64 string` and `Convert.FromBase64String` method. But I appreciate your help. –  Aug 05 '19 at 13:46