2

I've created a control derived from ComboBox, and wish to unit test its behaviour.

However, it appears to be behaving differently in my unit test to how it behaves in the real application.

In the real application, the Combobox.DataSource property and the .Items sync up - in other words when I change the Combobox.DataSource the .Items list immediately and automatically updates to show an item for each element of the DataSource.

In my test, I construct a ComboBox, assign a datasource to it, but the .Items list doesn't get updated at all, remaining at 0 items. Thus, when I try to update the .SelectedIndex to 0 in the test to select the first item, I recieve an ArgumentOutOfRangeException.

Is this because I don't have an Application.Run in my unit test starting an event loop, or is this a bit of a red herring?

EDIT: More detail on the first test:

    [SetUp]
    public void SetUp()
    {
        mECB = new EnhancedComboBox();

        mECB.FormattingEnabled = true;
        mECB.Location = new System.Drawing.Point( 45, 4 );
        mECB.Name = "cboFind";
        mECB.Size = new System.Drawing.Size( 121, 21 );
        mECB.TabIndex = 3;

        mECB.AddObserver( this );

        mTestItems = new List<TestItem>();
        mTestItems.Add( new TestItem() { Value = "Billy" } );
        mTestItems.Add( new TestItem() { Value = "Bob" } );
        mTestItems.Add( new TestItem() { Value = "Blues" } );

        mECB.DataSource = mTestItems;
        mECB.Reset();

        mObservedValue = null;
    }

    [Test]
    public void Test01_UpdateObserver()
    {
        mECB.SelectedIndex = 0;
        Assert.AreEqual( "Billy", mObservedValue.Value );
    }

The test fails on the first line, when trying to set the SelectedIndex to 0. On debugging, this appears to be because when the .DataSource is changed, the .Items collection is not updated to reflect this. However, on debugging the real application, the .Items collection is always updated when the .DataSource changes.

Surely I don't have to actually render the ComboBox in the test, I don't even have any drawing surfaces set up to render on to! Maybe the only answer I need is "How do I make the ComboBox update in the same way as when it is drawn, in a unit test scenario where I don't actually need to draw the box?"

Paul Smith
  • 1,044
  • 2
  • 13
  • 29

5 Answers5

2

Since you're simply calling the constructor, a lot of functionality of the combobox will not work. For example, the items will be filled when the ComboBox is drawn on screen, on a form. This does not happen when constructing it in a unit test.

Why do you want to write a unit test on that combobox?

Can't you seperate the logic which now is in the custom control? For example put this in a controller, and test that?

Why don't you test on the DataSource property instead of the Items collection?

Gerrie Schenck
  • 22,148
  • 20
  • 68
  • 95
  • I'm confused by "separate the logic which is now in the custom control? The custom control *is* the combobox. All I want to do is test the functionality I've added by subclassing, but the base combobox is behaving differently in the test. How do I make the combobox behave 'normally'? – Paul Smith Jan 26 '09 at 12:38
  • My answer says that a combobox is filled by events caused by being drawn on a form. If you write a test for your combobox as it is now, you are basically testing if it draws correctly. So isn't testing the DataSource property enough? – Gerrie Schenck Jan 26 '09 at 12:41
  • OK, some more detail is needed. My EnhancedComboBox maintains a list of observers which it notifies when the selected item changes. So, I make my unit test observe the combobox, and set SelectedIndex to 0 to select the first item in the box. This should result in my test being notified of the – Paul Smith Jan 26 '09 at 12:45
  • change in selected item. It doesn't work, because ".SelectedIndex=0" throws an exception. Debugging this, although the .DataSource has 3 items in it, the .Items is empty. In the real application, when the DataSource is changed, .Items catches up automatically so ".SelectedIndex=0" works. – Paul Smith Jan 26 '09 at 12:46
  • Ok, I understand it now. Still this will be very hard to do in a unit test, since it involves filling the items collection which is triggered by the combobox being drawn on a real form. Maybe you can look into specialized unit test frameworks for winforms, I don't have any experience with those. – Gerrie Schenck Jan 26 '09 at 12:51
  • It might help if you could tell me how you know that the .Items collection is filled on drawing - is there some documentation or blog post or whatever that details this? – Paul Smith Jan 26 '09 at 12:53
0

I did a little hack to allow this in my custom derived combobox:

public class EnhancedComboBox : ComboBox 
{

    [... the implementation]

    public void DoRefreshItems()
    {
        SetItemsCore(DataSource as IList);       
    }
}

The SetItemsCore function instructs the base combobox to load internal items with the provided list, it's what uses internally after the datasource changes.

This function never gets called when the control is not on a form, because there are lots of checks for CurrencyManagers and BindingContexts that are failing because this components, I believe, are provided by the parent form somehow.

Anyway, in the test, you have to call mECB.DoRefreshItems() just after the mECB.DataSource = mTestItems and everything should be fine if you only depend on the SelectedIndex and the Items property. Any other behavior like databinding is probably still not functional.

David Lay
  • 2,956
  • 4
  • 27
  • 48
0

I'm sure that Application.Run absence cannot affects any control's behavior

abatishchev
  • 98,240
  • 88
  • 296
  • 433
  • Then why *does* the ComboBox under test behave differently to that in the real application? – Paul Smith Jan 26 '09 at 12:52
  • I think Application.Run only launches this from as main in current thread. Code for Reflector: public static void Run() { ThreadContext.FromCurrent().RunMessageLoop(-1, newApplicationContext()); } – abatishchev Jan 26 '09 at 12:54
0

I'm having the same problem with a combo box where the items are data bound. My current solution is to create a Form in the test, add the combo box to the Controls collection, and then show the form in my test. Kind of ugly. All my combo box really does is list a bunch of TimeSpan objects, sorted, and with custom formatting of the TimeSpan values. It also has special behavior on keypress events. I tried extracting all the data and logic to a separate class but couldn't figure it out. There probably is a better solution but what I'm doing seems satisfactory.

To make testing easier, I created these classes in my test code:

    class TestCombo : DurationComboBox {
        public void SimulateKeyUp(Keys keys) { base.OnKeyUp(new KeyEventArgs(keys)); }
        public DataView DataView { get { return DataSource as DataView; } }
        public IEnumerable<DataRowView> Rows() { return (DataView as IEnumerable).Cast<DataRowView>(); }
        public IEnumerable<int> Minutes() { return Rows().Select(row => (int)row["Minutes"]); }
    }

    class Target {
        public TestCombo Combo { get; private set; }
        public Form Form { get; private set; }

        public Target() {
            Combo = new TestCombo();
            Form = new Form();
            Form.Controls.Add(Combo);
            Form.Show();
        }
    }

Here is a sample test:

           [TestMethod()]
    public void ConstructorCreatesEmptyList() {
        Target t = new Target();
        Assert.AreEqual<int>(0, t.Combo.DataView.Count);
        Assert.AreEqual<int>(-1, t.Combo.SelectedMinutes);
        Assert.IsNull(t.Combo.SelectedItem);
    }
0

This solve some problems if target is ComboBox or any other control:

target.CreateControl();

but I was unable to set SelectedValue it has null value, my test working with two data sources for combo box, one as data source and second is binded to selevted value. With other controls everithing working fine. In the begining I was also creating form in tests, but there is problem when form on created on our build server while tests are executed.