Is the locking done before the select determines the max value, or
after
Before, by the query optimizer before it is executed. It wouldn't help to do it after, after all. This is the locking method for statement you are executing.
Is there a race condition with insert statements inserting into table
"table1"
No, since you are using TABLOCKX and not just TABLOCK. The latter allows shared locks, but you are getting an exclusive lock on the table via TABLOCKX.
Is it guaranteed that @foo will contain the max value found in the
table until the transaction is committed or rolled back
Yup, all other transactions will be blocked (deletes, inserts, updates, etc)
TESTING
To test this, create a table an insert a value
create table t1_delete (col1 int)
insert into t1_delete
values (1)
go
Next, in one SSMS panel run your code but comment out the COMMIT TRAN
begin Transaction
declare @foo int = (select max(col1) from t1_delete with (tablockx))
-- Is it possible that max(col1) can be > @foo here?
select @foo
--Commit Transaction
Now, in a new SSMS window try to insert a new value, or whatever
insert into t1_delete
values(2)
You will notice the query spins. You can see why if you run exec sp_whoIsActive from Adam Mechanic in another query window. Specifically, check out the blocking_session_id for your insert session. It will be the session with sql_test like begin transaction declare @foo....
Don't forget to commit the transaction after the test